feat: add picker server loader. Set chevron image for expand button
This commit is contained in:
parent
0228f409a8
commit
46baa59d37
|
@ -199,7 +199,8 @@
|
|||
},
|
||||
"empty_state": {
|
||||
"finding_servers": "Finding available servers...",
|
||||
"bad_network": "Something went wrong while loading data. Check your internet connection."
|
||||
"bad_network": "Something went wrong while loading data. Check your internet connection.",
|
||||
"no_results": "No results"
|
||||
}
|
||||
},
|
||||
"register": {
|
||||
|
|
|
@ -191,6 +191,7 @@
|
|||
DB02CDBF2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB02CDBE2625AE5000D0A2AF /* AdaptiveUserInterfaceStyleBarButtonItem.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 */; };
|
||||
DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */; };
|
||||
DB118A8225E4B6E600FAB162 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */; };
|
||||
DB118A8C25E4BFB500FAB162 /* HighlightDimmableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */; };
|
||||
DB1D186C25EF5BA7003F1F23 /* PollTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */; };
|
||||
|
@ -758,6 +759,7 @@
|
|||
DB0AC6FB25CD02E600D75117 /* APIService+Instance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Instance.swift"; sourceTree = "<group>"; };
|
||||
DB0F814D264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
DB0F814E264CFFD300F2A12B /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||
DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickServerLoaderTableViewCell.swift; sourceTree = "<group>"; };
|
||||
DB118A8125E4B6E600FAB162 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
DB118A8B25E4BFB500FAB162 /* HighlightDimmableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightDimmableButton.swift; sourceTree = "<group>"; };
|
||||
DB1D186B25EF5BA7003F1F23 /* PollTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollTableView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1136,6 +1138,7 @@
|
|||
0FB3D30725E524C600AAD544 /* PickServerCategoriesCell.swift */,
|
||||
0FB3D33125E5F50E00AAD544 /* PickServerSearchCell.swift */,
|
||||
0FB3D33725E6401400AAD544 /* PickServerCell.swift */,
|
||||
DB0F814F264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift */,
|
||||
);
|
||||
path = TableViewCell;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1390,6 +1393,7 @@
|
|||
2D7631A425C1532200929FB9 /* Share */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5D03938E2612D200007FE196 /* Webview */,
|
||||
DB68A04F25E9028800CFDF14 /* NavigationController */,
|
||||
DB9D6C2025E502C60051B173 /* ViewModel */,
|
||||
2D7631A525C1532D00929FB9 /* View */,
|
||||
|
@ -2047,7 +2051,6 @@
|
|||
DB8AF55525C1379F002E6C99 /* Scene */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5D03938E2612D200007FE196 /* Webview */,
|
||||
2D7631A425C1532200929FB9 /* Share */,
|
||||
DB6180E426391A500018D199 /* Transition */,
|
||||
DB8AF54E25C13703002E6C99 /* MainTab */,
|
||||
|
@ -2983,6 +2986,7 @@
|
|||
DBAE3FAF26172FC0004B8251 /* RemoteProfileViewModel.swift in Sources */,
|
||||
DBE3CE0D261D767100430CC6 /* FavoriteViewController+Provider.swift in Sources */,
|
||||
2D084B9326259545003AA3AF /* NotificationViewModel+LoadLatestState.swift in Sources */,
|
||||
DB0F8150264D1E2500F2A12B /* PickServerLoaderTableViewCell.swift in Sources */,
|
||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
||||
DB9E0D6F25EE008500CFDD76 /* UIInterpolatingMotionEffect.swift in Sources */,
|
||||
DBB9759C262462E1004620BD /* ThreadMetaView.swift in Sources */,
|
||||
|
|
|
@ -14,6 +14,7 @@ enum PickServerItem {
|
|||
case categoryPicker(items: [CategoryPickerItem])
|
||||
case search
|
||||
case server(server: Mastodon.Entity.Server, attribute: ServerItemAttribute)
|
||||
case loader(attribute: LoaderItemAttribute)
|
||||
}
|
||||
|
||||
extension PickServerItem {
|
||||
|
@ -34,6 +35,26 @@ extension PickServerItem {
|
|||
hasher.combine(isExpand)
|
||||
}
|
||||
}
|
||||
|
||||
final class LoaderItemAttribute: Equatable, Hashable {
|
||||
let id = UUID()
|
||||
|
||||
var isLast: Bool
|
||||
var isNoResult: Bool
|
||||
|
||||
init(isLast: Bool, isEmptyResult: Bool) {
|
||||
self.isLast = isLast
|
||||
self.isNoResult = isEmptyResult
|
||||
}
|
||||
|
||||
static func == (lhs: PickServerItem.LoaderItemAttribute, rhs: PickServerItem.LoaderItemAttribute) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PickServerItem: Equatable {
|
||||
|
@ -47,6 +68,8 @@ extension PickServerItem: Equatable {
|
|||
return true
|
||||
case (.server(let serverLeft, _), .server(let serverRight, _)):
|
||||
return serverLeft.domain == serverRight.domain
|
||||
case (.loader(let attributeLeft), loader(let attributeRight)):
|
||||
return attributeLeft == attributeRight
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
@ -64,6 +87,8 @@ extension PickServerItem: Hashable {
|
|||
hasher.combine(String(describing: PickServerItem.search.self))
|
||||
case .server(let server, _):
|
||||
hasher.combine(server.domain)
|
||||
case .loader(let attribute):
|
||||
hasher.combine(attribute)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,6 +57,10 @@ extension PickServerSection {
|
|||
PickServerSection.configure(cell: cell, server: server, attribute: attribute)
|
||||
cell.delegate = pickServerCellDelegate
|
||||
return cell
|
||||
case .loader(let attribute):
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: PickServerLoaderTableViewCell.self), for: indexPath) as! PickServerLoaderTableViewCell
|
||||
PickServerSection.configure(cell: cell, attribute: attribute)
|
||||
return cell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -137,3 +141,23 @@ extension PickServerSection {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
extension PickServerSection {
|
||||
|
||||
static func configure(cell: PickServerLoaderTableViewCell, attribute: PickServerItem.LoaderItemAttribute) {
|
||||
if attribute.isLast {
|
||||
cell.containerView.layer.maskedCorners = [
|
||||
.layerMinXMaxYCorner,
|
||||
.layerMaxXMaxYCorner
|
||||
]
|
||||
cell.containerView.layer.cornerCurve = .continuous
|
||||
cell.containerView.layer.cornerRadius = MastodonPickServerAppearance.tableViewCornerRadius
|
||||
} else {
|
||||
cell.containerView.layer.cornerRadius = 0
|
||||
}
|
||||
|
||||
attribute.isNoResult ? cell.stopAnimating() : cell.startAnimating()
|
||||
cell.emptyStatusLabel.isHidden = !attribute.isNoResult
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -773,6 +773,8 @@ internal enum L10n {
|
|||
internal static let badNetwork = L10n.tr("Localizable", "Scene.ServerPicker.EmptyState.BadNetwork")
|
||||
/// Finding available servers...
|
||||
internal static let findingServers = L10n.tr("Localizable", "Scene.ServerPicker.EmptyState.FindingServers")
|
||||
/// No results
|
||||
internal static let noResults = L10n.tr("Localizable", "Scene.ServerPicker.EmptyState.NoResults")
|
||||
}
|
||||
internal enum Input {
|
||||
/// Find a server or join your own...
|
||||
|
|
|
@ -257,6 +257,7 @@ tap the link to confirm your account.";
|
|||
"Scene.ServerPicker.Button.SeeMore" = "See More";
|
||||
"Scene.ServerPicker.EmptyState.BadNetwork" = "Something went wrong while loading data. Check your internet connection.";
|
||||
"Scene.ServerPicker.EmptyState.FindingServers" = "Finding available servers...";
|
||||
"Scene.ServerPicker.EmptyState.NoResults" = "No results";
|
||||
"Scene.ServerPicker.Input.Placeholder" = "Find a server or join your own...";
|
||||
"Scene.ServerPicker.Label.Category" = "CATEGORY";
|
||||
"Scene.ServerPicker.Label.Language" = "LANGUAGE";
|
||||
|
|
|
@ -257,6 +257,7 @@ tap the link to confirm your account.";
|
|||
"Scene.ServerPicker.Button.SeeMore" = "See More";
|
||||
"Scene.ServerPicker.EmptyState.BadNetwork" = "Something went wrong while loading data. Check your internet connection.";
|
||||
"Scene.ServerPicker.EmptyState.FindingServers" = "Finding available servers...";
|
||||
"Scene.ServerPicker.EmptyState.NoResults" = "No results";
|
||||
"Scene.ServerPicker.Input.Placeholder" = "Find a server or join your own...";
|
||||
"Scene.ServerPicker.Label.Category" = "CATEGORY";
|
||||
"Scene.ServerPicker.Label.Language" = "LANGUAGE";
|
||||
|
|
|
@ -31,6 +31,7 @@ final class MastodonPickServerViewController: UIViewController, NeedsDependency
|
|||
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
|
||||
|
|
|
@ -39,7 +39,7 @@ class MastodonPickServerViewModel: NSObject {
|
|||
let selectCategoryItem = CurrentValueSubject<CategoryPickerItem, Never>(.all)
|
||||
let searchText = CurrentValueSubject<String, Never>("")
|
||||
let indexedServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([])
|
||||
let unindexedServers = CurrentValueSubject<[Mastodon.Entity.Server], Never>([])
|
||||
let unindexedServers = CurrentValueSubject<[Mastodon.Entity.Server]?, Never>([]) // set nil when loading
|
||||
let viewWillAppear = PassthroughSubject<Void, Never>()
|
||||
|
||||
// output
|
||||
|
@ -85,8 +85,8 @@ extension MastodonPickServerViewModel {
|
|||
|
||||
private func configure() {
|
||||
Publishers.CombineLatest(
|
||||
filteredIndexedServers.eraseToAnyPublisher(),
|
||||
unindexedServers.eraseToAnyPublisher()
|
||||
filteredIndexedServers,
|
||||
unindexedServers
|
||||
)
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink(receiveValue: { [weak self] indexedServers, unindexedServers in
|
||||
|
@ -114,16 +114,31 @@ extension MastodonPickServerViewModel {
|
|||
guard !serverItems.contains(item) else { continue }
|
||||
serverItems.append(item)
|
||||
}
|
||||
for server in unindexedServers {
|
||||
let attribute = oldSnapshotServerItemAttributeDict[server.domain] ?? PickServerItem.ServerItemAttribute(isLast: false, isExpand: false)
|
||||
attribute.isLast = 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 = 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 = true
|
||||
}
|
||||
if case let .loader(attribute) = serverItems.last {
|
||||
attribute.isLast = true
|
||||
}
|
||||
snapshot.appendItems(serverItems, toSection: .servers)
|
||||
|
||||
diffableDataSource.defaultRowAnimation = .fade
|
||||
|
@ -168,6 +183,7 @@ extension MastodonPickServerViewModel {
|
|||
guard let domain = AuthenticationViewModel.parseDomain(from: searchText) else {
|
||||
return Just(Result.failure(APIService.APIError.implicit(.badRequest))).eraseToAnyPublisher()
|
||||
}
|
||||
self.unindexedServers.value = nil
|
||||
return self.context.apiService.instance(domain: domain)
|
||||
.map { response -> Result<Mastodon.Response.Content<[Mastodon.Entity.Server]>, Error>in
|
||||
let newResponse = response.map { [Mastodon.Entity.Server(instance: $0)] }
|
||||
|
@ -184,9 +200,14 @@ extension MastodonPickServerViewModel {
|
|||
switch result {
|
||||
case .success(let response):
|
||||
self.unindexedServers.send(response.value)
|
||||
case .failure:
|
||||
// TODO: What should be presented when user inputs invalid search text?
|
||||
self.unindexedServers.send([])
|
||||
case .failure(let error):
|
||||
if let error = error as? APIService.APIError,
|
||||
case let .implicit(reason) = error,
|
||||
case .badRequest = reason {
|
||||
self.unindexedServers.send([])
|
||||
} else {
|
||||
self.unindexedServers.send(nil)
|
||||
}
|
||||
}
|
||||
})
|
||||
.store(in: &disposeBag)
|
||||
|
|
|
@ -88,11 +88,14 @@ class PickServerCell: UITableViewCell {
|
|||
|
||||
let expandButton: UIButton = {
|
||||
let button = UIButton(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.setTitle(L10n.Scene.ServerPicker.Button.seeLess, for: .selected)
|
||||
button.setTitleColor(Asset.Colors.brandBlue.color, for: .normal)
|
||||
button.titleLabel?.font = .preferredFont(forTextStyle: .footnote)
|
||||
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
|
||||
}()
|
||||
|
||||
|
@ -325,11 +328,15 @@ extension PickServerCell {
|
|||
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)
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
//
|
||||
// PickServerLoaderTableViewCell.swift
|
||||
// Mastodon
|
||||
//
|
||||
// Created by MainasuK Cirno on 2021-5-13.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
final class PickServerLoaderTableViewCell: TimelineLoaderTableViewCell {
|
||||
|
||||
let containerView: UIView = {
|
||||
let view = UIView()
|
||||
view.layoutMargins = UIEdgeInsets(top: 16, left: 16, bottom: 10, right: 16)
|
||||
view.backgroundColor = Asset.Colors.Background.secondaryGroupedSystemBackground.color
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
let seperator: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Asset.Colors.Background.systemGroupedBackground.color
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
return view
|
||||
}()
|
||||
|
||||
let emptyStatusLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
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)
|
||||
return label
|
||||
}()
|
||||
|
||||
override func _init() {
|
||||
super._init()
|
||||
|
||||
contentView.addSubview(containerView)
|
||||
contentView.addSubview(seperator)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
// Set background view
|
||||
containerView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||
containerView.leadingAnchor.constraint(equalTo: contentView.readableContentGuide.leadingAnchor),
|
||||
contentView.readableContentGuide.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),
|
||||
])
|
||||
|
||||
emptyStatusLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
containerView.addSubview(emptyStatusLabel)
|
||||
NSLayoutConstraint.activate([
|
||||
emptyStatusLabel.leadingAnchor.constraint(equalTo: containerView.readableContentGuide.leadingAnchor),
|
||||
containerView.readableContentGuide.trailingAnchor.constraint(equalTo: emptyStatusLabel.trailingAnchor),
|
||||
emptyStatusLabel.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
|
||||
])
|
||||
emptyStatusLabel.isHidden = true
|
||||
|
||||
contentView.bringSubviewToFront(stackView)
|
||||
activityIndicatorView.isHidden = false
|
||||
startAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
#if canImport(SwiftUI) && DEBUG
|
||||
import SwiftUI
|
||||
|
||||
struct PickServerLoaderTableViewCell_Previews: PreviewProvider {
|
||||
|
||||
static var previews: some View {
|
||||
UIViewPreview(width: 375) {
|
||||
PickServerLoaderTableViewCell()
|
||||
}
|
||||
.previewLayout(.fixed(width: 375, height: 100))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
|
@ -16,9 +16,9 @@ class TimelineLoaderTableViewCell: UITableViewCell {
|
|||
static let labelFont = UIFontMetrics(forTextStyle: .body).scaledFont(for: .systemFont(ofSize: 17, weight: .medium))
|
||||
|
||||
var disposeBag = Set<AnyCancellable>()
|
||||
|
||||
var stateBindDispose: AnyCancellable?
|
||||
|
||||
|
||||
let stackView = UIStackView()
|
||||
|
||||
let loadMoreButton: UIButton = {
|
||||
let button = HighlightDimmableButton()
|
||||
button.titleLabel?.font = TimelineLoaderTableViewCell.labelFont
|
||||
|
@ -86,7 +86,6 @@ class TimelineLoaderTableViewCell: UITableViewCell {
|
|||
])
|
||||
|
||||
// use stack view to alignlment content center
|
||||
let stackView = UIStackView()
|
||||
stackView.spacing = 4
|
||||
stackView.axis = .horizontal
|
||||
stackView.alignment = .center
|
||||
|
|
Loading…
Reference in New Issue