This commit is contained in:
Justin Mazzocchi 2021-01-20 15:33:53 -08:00
parent 6dd991f086
commit 02747215c5
No known key found for this signature in database
GPG key ID: E223E6937AAFB01C
12 changed files with 514 additions and 42 deletions

View file

@ -18,7 +18,7 @@ public extension Timeline {
typealias Id = String
static let unauthenticatedDefaults: [Timeline] = [.local, .federated]
static let authenticatedDefaults: [Timeline] = [.home, .local, .federated, .favorites, .bookmarks]
static let authenticatedDefaults: [Timeline] = [.home, .local, .federated]
var filterContext: Filter.Context? {
switch self {

View file

@ -0,0 +1,33 @@
// Copyright © 2021 Metabolist. All rights reserved.
import Foundation
import UIKit
import ViewModels
extension NavigationViewModel.Tab {
var title: String {
switch self {
case .timelines:
return NSLocalizedString("main-navigation.timelines", comment: "")
case .explore:
return NSLocalizedString("main-navigation.explore", comment: "")
case .notifications:
return NSLocalizedString("main-navigation.notifications", comment: "")
case .messages:
return NSLocalizedString("main-navigation.conversations", comment: "")
}
}
var systemImageName: String {
switch self {
case .timelines: return "newspaper"
case .explore: return "magnifyingglass"
case .notifications: return "bell"
case .messages: return "envelope"
}
}
var tabBarItem: UITabBarItem {
UITabBarItem(title: title, image: UIImage(systemName: systemImageName), selectedImage: nil)
}
}

View file

@ -88,6 +88,10 @@
"identities.pending" = "Pending";
"lists.new-list-title" = "New List Title";
"load-more" = "Load More";
"main-navigation.timelines" = "Timelines";
"main-navigation.explore" = "Explore";
"main-navigation.notifications" = "Notifications";
"main-navigation.conversations" = "Conversations";
"messages" = "Messages";
"ok" = "OK";
"pending.pending-confirmation" = "Your account is pending confirmation";
@ -201,5 +205,8 @@
"status.visibility.direct.description" = "Visible for mentioned users only";
"submit" = "Submit";
"timelines.home" = "Home";
"timelines.home.description" = "Posts from accounts you're following";
"timelines.local" = "Local";
"timelines.local.description-%@" = "Public posts on %@";
"timelines.federated" = "Federated";
"timelines.federated.description-%@" = "Public posts on instances known by %@";

View file

@ -23,6 +23,11 @@
D01F41D924F880C400D55A2D /* TouchFallthroughTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */; };
D01F41E424F8889700D55A2D /* AttachmentsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01F41E224F8889700D55A2D /* AttachmentsView.swift */; };
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D02E1F94250B13210071AD56 /* SafariView.swift */; };
D035F86925B7F2ED00DC75ED /* MainNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F86825B7F2ED00DC75ED /* MainNavigationViewController.swift */; };
D035F86F25B7F30E00DC75ED /* MainNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F86E25B7F30E00DC75ED /* MainNavigationView.swift */; };
D035F87D25B7F61600DC75ED /* TimelinesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F87C25B7F61600DC75ED /* TimelinesViewController.swift */; };
D035F88725B8016000DC75ED /* NavigationViewModel+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F88625B8016000DC75ED /* NavigationViewModel+Extensions.swift */; };
D035F89125B8067100DC75ED /* TimelinesTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F89025B8067100DC75ED /* TimelinesTitleView.swift */; };
D036AA02254B6101009094DF /* NotificationListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA01254B6101009094DF /* NotificationListCell.swift */; };
D036AA07254B6118009094DF /* NotificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA06254B6118009094DF /* NotificationView.swift */; };
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */; };
@ -204,6 +209,11 @@
D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TouchFallthroughTextView.swift; sourceTree = "<group>"; };
D01F41E224F8889700D55A2D /* AttachmentsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttachmentsView.swift; sourceTree = "<group>"; };
D02E1F94250B13210071AD56 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = "<group>"; };
D035F86825B7F2ED00DC75ED /* MainNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNavigationViewController.swift; sourceTree = "<group>"; };
D035F86E25B7F30E00DC75ED /* MainNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainNavigationView.swift; sourceTree = "<group>"; };
D035F87C25B7F61600DC75ED /* TimelinesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesViewController.swift; sourceTree = "<group>"; };
D035F88625B8016000DC75ED /* NavigationViewModel+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NavigationViewModel+Extensions.swift"; sourceTree = "<group>"; };
D035F89025B8067100DC75ED /* TimelinesTitleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesTitleView.swift; sourceTree = "<group>"; };
D036AA01254B6101009094DF /* NotificationListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationListCell.swift; sourceTree = "<group>"; };
D036AA06254B6118009094DF /* NotificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationView.swift; sourceTree = "<group>"; };
D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentConfiguration.swift; sourceTree = "<group>"; };
@ -504,6 +514,7 @@
D0B8510B25259E56004E0744 /* LoadMoreCell.swift */,
D0E569DF252931B100FA1D72 /* LoadMoreContentConfiguration.swift */,
D0E569DA2529319100FA1D72 /* LoadMoreView.swift */,
D035F86E25B7F30E00DC75ED /* MainNavigationView.swift */,
D05936FE25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift */,
D03B1B29253818F3008F964B /* MediaPreferencesView.swift */,
D0FCC10F259C4F20000B67DF /* NewStatusView.swift */,
@ -526,6 +537,7 @@
D0625E55250F086B00502611 /* Status */,
D0C7D42524F76169001EBDBB /* TableView.swift */,
D0C7D42E24F76169001EBDBB /* TabNavigationView.swift */,
D035F89025B8067100DC75ED /* TimelinesTitleView.swift */,
D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */,
D0EA59472522B8B600804347 /* ViewConstants.swift */,
D0F2D54A2581CF7D00986197 /* VisualEffectBlur.swift */,
@ -542,9 +554,11 @@
D08B8D49253FC36500B1EBEF /* ImageNavigationController.swift */,
D08B8D41253F92B600B1EBEF /* ImagePageViewController.swift */,
D08B8D3C253F929E00B1EBEF /* ImageViewController.swift */,
D035F86825B7F2ED00DC75ED /* MainNavigationViewController.swift */,
D0FCC104259C4E61000B67DF /* NewStatusViewController.swift */,
D06BC5E525202AD90079541D /* ProfileViewController.swift */,
D0F0B12D251A97E400942152 /* TableViewController.swift */,
D035F87C25B7F61600DC75ED /* TimelinesViewController.swift */,
);
path = "View Controllers";
sourceTree = "<group>";
@ -574,6 +588,7 @@
D05E688425B55AE8001FB2C6 /* AVURLAsset+Extensions.swift */,
D0F0B135251AA12700942152 /* CollectionItem+Extensions.swift */,
D0C7D46E24F76169001EBDBB /* KingfisherOptionsInfo+Extensions.swift */,
D035F88625B8016000DC75ED /* NavigationViewModel+Extensions.swift */,
D0C7D46B24F76169001EBDBB /* NSMutableAttributedString+Extensions.swift */,
D07EC7CE25B13921006DF726 /* PickerEmoji+Extensions.swift */,
D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */,
@ -856,6 +871,7 @@
D08B8D822544D80000B1EBEF /* PollOptionButton.swift in Sources */,
D0C7D4DA24F7616A001EBDBB /* View+Extensions.swift in Sources */,
D07EC81125B232C2006DF726 /* SystemEmoji+Extensions.swift in Sources */,
D035F87D25B7F61600DC75ED /* TimelinesViewController.swift in Sources */,
D059373325AAEA7000754FDF /* CompositionPollView.swift in Sources */,
D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */,
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
@ -874,7 +890,9 @@
D0C7D49724F7616A001EBDBB /* IdentitiesView.swift in Sources */,
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */,
D059373E25AB8D5200754FDF /* CompositionPollOptionView.swift in Sources */,
D035F89125B8067100DC75ED /* TimelinesTitleView.swift in Sources */,
D036AA17254CA824009094DF /* StatusBodyView.swift in Sources */,
D035F86F25B7F30E00DC75ED /* MainNavigationView.swift in Sources */,
D08E512125786A6600FA2C5F /* UIButton+Extensions.swift in Sources */,
D05936F425AA66A600754FDF /* UIView+Extensions.swift in Sources */,
D05936E925AA3F3D00754FDF /* EditAttachmentView.swift in Sources */,
@ -883,6 +901,7 @@
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */,
D0C7D49824F7616A001EBDBB /* CustomEmojiText.swift in Sources */,
D08B8D3D253F929E00B1EBEF /* ImageViewController.swift in Sources */,
D035F86925B7F2ED00DC75ED /* MainNavigationViewController.swift in Sources */,
D0B8510C25259E56004E0744 /* LoadMoreCell.swift in Sources */,
D08E52612579D2E100FA2C5F /* DomainBlocksView.swift in Sources */,
D01F41E424F8889700D55A2D /* AttachmentsView.swift in Sources */,
@ -901,6 +920,7 @@
D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */,
D0E1F583251F13EC00D45315 /* WebfingerIndicatorView.swift in Sources */,
D0BEB20524FA1107001B0F04 /* FiltersView.swift in Sources */,
D035F88725B8016000DC75ED /* NavigationViewModel+Extensions.swift in Sources */,
D0FCC110259C4F20000B67DF /* NewStatusView.swift in Sources */,
D0C7D49B24F7616A001EBDBB /* PreferencesView.swift in Sources */,
D088406D25AFBBE200BB749B /* EmojiPickerViewController.swift in Sources */,

View file

@ -0,0 +1,64 @@
// Copyright © 2021 Metabolist. All rights reserved.
import UIKit
import ViewModels
final class MainNavigationViewController: UITabBarController {
private let viewModel: NavigationViewModel
private let rootViewModel: RootViewModel
init(viewModel: NavigationViewModel, rootViewModel: RootViewModel) {
self.viewModel = viewModel
self.rootViewModel = rootViewModel
super.init(nibName: nil, bundle: nil)
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
let timelinesViewController = TimelinesViewController(
viewModel: viewModel,
rootViewModel: rootViewModel)
let timelinesNavigationController = UINavigationController(rootViewController: timelinesViewController)
if let notificationsViewModel = viewModel.notificationsViewModel,
let conversationsViewModel = viewModel.conversationsViewModel {
let notificationsViewController = TableViewController(
viewModel: notificationsViewModel,
rootViewModel: rootViewModel,
identification: viewModel.identification)
notificationsViewController.tabBarItem = NavigationViewModel.Tab.notifications.tabBarItem
let notificationsNavigationViewController = UINavigationController(
rootViewController: notificationsViewController)
let conversationsViewController = TableViewController(
viewModel: conversationsViewModel,
rootViewModel: rootViewModel,
identification: viewModel.identification)
conversationsViewController.tabBarItem = NavigationViewModel.Tab.messages.tabBarItem
conversationsViewController.navigationItem.title = NavigationViewModel.Tab.messages.title
let conversationsNavigationViewController = UINavigationController(
rootViewController: conversationsViewController)
viewControllers = [
timelinesNavigationController,
notificationsNavigationViewController,
conversationsNavigationViewController
]
} else {
viewControllers = [
timelinesNavigationController
]
}
}
}

View file

@ -0,0 +1,131 @@
// Copyright © 2021 Metabolist. All rights reserved.
import Combine
import UIKit
import ViewModels
final class TimelinesViewController: UIPageViewController {
private let titleView: TimelinesTitleView
private let timelineViewControllers: [TableViewController]
private let viewModel: NavigationViewModel
private let rootViewModel: RootViewModel
private var cancellables = Set<AnyCancellable>()
init(viewModel: NavigationViewModel, rootViewModel: RootViewModel) {
self.viewModel = viewModel
self.rootViewModel = rootViewModel
let timelineViewModels: [CollectionViewModel]
if let homeTimelineViewModel = viewModel.homeTimelineViewModel {
timelineViewModels = [
homeTimelineViewModel,
viewModel.localTimelineViewModel,
viewModel.federatedTimelineViewModel]
} else {
timelineViewModels = [
viewModel.localTimelineViewModel,
viewModel.federatedTimelineViewModel]
}
titleView = TimelinesTitleView(
timelines: viewModel.identification.identity.authenticated
? Timeline.authenticatedDefaults
: Timeline.unauthenticatedDefaults,
identification: viewModel.identification)
timelineViewControllers = timelineViewModels.map {
TableViewController(
viewModel: $0,
rootViewModel: rootViewModel,
identification: viewModel.identification)
}
super.init(transitionStyle: .scroll,
navigationOrientation: .horizontal,
options: [.interPageSpacing: CGFloat.defaultSpacing])
if let firstViewController = timelineViewControllers.first {
setViewControllers([firstViewController], direction: .forward, animated: false)
}
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
dataSource = self
delegate = self
tabBarItem = UITabBarItem(
title: NSLocalizedString("main-navigation.timelines", comment: ""),
image: UIImage(systemName: "newspaper"),
selectedImage: nil)
navigationItem.titleView = titleView
navigationItem.leftBarButtonItem = UIBarButtonItem(systemItem: .close)
navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "megaphone"), primaryAction: nil)
titleView.$selectedTimeline
.compactMap { [weak self] in self?.titleView.timelines.firstIndex(of: $0) }
.sink { [weak self] index in
guard let self = self,
let currentViewController = self.viewControllers?.first as? TableViewController,
let currentIndex = self.timelineViewControllers.firstIndex(of: currentViewController),
index != currentIndex
else { return }
self.setViewControllers(
[self.timelineViewControllers[index]],
direction: index > currentIndex ? .forward : .reverse,
animated: !UIAccessibility.isReduceMotionEnabled)
}
.store(in: &cancellables)
}
}
extension TimelinesViewController: UIPageViewControllerDataSource {
func pageViewController(_ pageViewController: UIPageViewController,
viewControllerAfter viewController: UIViewController) -> UIViewController? {
guard
let timelineViewController = viewController as? TableViewController,
let index = timelineViewControllers.firstIndex(of: timelineViewController),
index + 1 < timelineViewControllers.count
else { return nil }
return timelineViewControllers[index + 1]
}
func pageViewController(_ pageViewController: UIPageViewController,
viewControllerBefore viewController: UIViewController) -> UIViewController? {
guard
let timelineViewController = viewController as? TableViewController,
let index = timelineViewControllers.firstIndex(of: timelineViewController),
index > 0
else { return nil }
return timelineViewControllers[index - 1]
}
}
extension TimelinesViewController: UIPageViewControllerDelegate {
func pageViewController(_ pageViewController: UIPageViewController,
didFinishAnimating finished: Bool,
previousViewControllers: [UIViewController],
transitionCompleted completed: Bool) {
guard let viewController = viewControllers?.first as? TableViewController,
let index = timelineViewControllers.firstIndex(of: viewController)
else { return }
let timeline = titleView.timelines[index]
if titleView.selectedTimeline != timeline {
titleView.selectedTimeline = timeline
}
}
}

View file

@ -21,38 +21,56 @@ public final class NavigationViewModel: ObservableObject {
@Published public var alertItem: AlertItem?
public private(set) var timelineViewModel: CollectionItemsViewModel
public var notificationsViewModel: CollectionViewModel? {
public lazy var homeTimelineViewModel: CollectionViewModel? = {
if identification.identity.authenticated {
if _notificationsViewModel == nil {
_notificationsViewModel = CollectionItemsViewModel(
return CollectionItemsViewModel(
collectionService: identification.service.service(timeline: .home),
identification: identification)
}
return nil
}()
public lazy var localTimelineViewModel: CollectionViewModel = {
CollectionItemsViewModel(
collectionService: identification.service.service(timeline: .local),
identification: identification)
}()
public lazy var federatedTimelineViewModel: CollectionViewModel = {
CollectionItemsViewModel(
collectionService: identification.service.service(timeline: .federated),
identification: identification)
}()
public lazy var notificationsViewModel: CollectionViewModel? = {
if identification.identity.authenticated {
let notificationsViewModel = CollectionItemsViewModel(
collectionService: identification.service.notificationsService(),
identification: identification)
_notificationsViewModel?.request(maxId: nil, minId: nil)
}
return _notificationsViewModel
notificationsViewModel.request(maxId: nil, minId: nil)
return notificationsViewModel
} else {
return nil
}
}
}()
public var conversationsViewModel: CollectionViewModel? {
public lazy var conversationsViewModel: CollectionViewModel? = {
if identification.identity.authenticated {
if _conversationsViewModel == nil {
_conversationsViewModel = CollectionItemsViewModel(
let conversationsViewModel = CollectionItemsViewModel(
collectionService: identification.service.conversationsService(),
identification: identification)
_conversationsViewModel?.request(maxId: nil, minId: nil)
}
return _conversationsViewModel
conversationsViewModel.request(maxId: nil, minId: nil)
return conversationsViewModel
} else {
return nil
}
}
}()
private var _notificationsViewModel: CollectionViewModel?
private var _conversationsViewModel: CollectionViewModel?
private var cancellables = Set<AnyCancellable>()
public init(identification: Identification) {

View file

@ -0,0 +1,32 @@
// Copyright © 2021 Metabolist. All rights reserved.
import SwiftUI
import ViewModels
struct MainNavigationView: UIViewControllerRepresentable {
let viewModelClosure: () -> NavigationViewModel
@EnvironmentObject var rootViewModel: RootViewModel
@EnvironmentObject var identification: Identification
func makeUIViewController(context: Context) -> MainNavigationViewController {
MainNavigationViewController(
viewModel: viewModelClosure(),
rootViewModel: rootViewModel)
}
func updateUIViewController(_ uiViewController: MainNavigationViewController, context: Context) {
}
}
#if DEBUG
import PreviewViewModels
struct MainNavigationView_Previews: PreviewProvider {
static var previews: some View {
MainNavigationView { NavigationViewModel(identification: .preview) }
.environmentObject(Identification.preview)
.environmentObject(RootViewModel.preview)
}
}
#endif

View file

@ -8,10 +8,11 @@ struct RootView: View {
var body: some View {
if let navigationViewModel = viewModel.navigationViewModel {
TabNavigationView(viewModel: navigationViewModel)
MainNavigationView { navigationViewModel }
.id(navigationViewModel.identification.identity.id)
.environmentObject(viewModel)
.transition(.opacity)
.edgesIgnoringSafeArea(.all)
} else {
NavigationView {
AddIdentityView(viewModel: viewModel.addIdentityViewModel())

View file

@ -188,7 +188,8 @@ private extension TabNavigationView {
}
}
private extension Timeline {
// TODO: move
extension Timeline {
var title: String {
switch self {
case .home:
@ -210,10 +211,40 @@ private extension Timeline {
}
}
func subtitle(identification: Identification) -> String? {
switch self {
case .home:
return identification.identity.handle
default:
return identification.identity.instance?.uri
}
}
func description(instanceName: String?) -> String? {
switch self {
case .home:
return NSLocalizedString("timelines.home.description", comment: "")
case .local:
guard let instanceName = instanceName else { return nil }
return String.localizedStringWithFormat(
NSLocalizedString("timelines.local.description-%@", comment: ""),
instanceName)
case .federated:
guard let instanceName = instanceName else { return nil }
return String.localizedStringWithFormat(
NSLocalizedString("timelines.federated.description-%@", comment: ""),
instanceName)
default:
return nil
}
}
var systemImageName: String {
switch self {
case .home: return "house"
case .local: return "person.3"
case .local: return "building.2.crop.circle"
case .federated: return "network"
case .list: return "scroll"
case .tag: return "number"
@ -224,26 +255,6 @@ private extension Timeline {
}
}
extension NavigationViewModel.Tab {
var title: String {
switch self {
case .timelines: return "Timelines"
case .explore: return "Explore"
case .notifications: return "Notifications"
case .messages: return "Messages"
}
}
var systemImageName: String {
switch self {
case .timelines: return "newspaper"
case .explore: return "magnifyingglass"
case .notifications: return "bell"
case .messages: return "envelope"
}
}
}
#if DEBUG
import PreviewViewModels

View file

@ -0,0 +1,153 @@
// Copyright © 2021 Metabolist. All rights reserved.
import Combine
import UIKit
import ViewModels
final class TimelinesTitleView: UIControl {
let timelines: [Timeline]
private let titleLabel = UILabel()
private let subtitleLabel = UILabel()
private let imageView = UIImageView()
private let chevronImageView = UIImageView(image: TimelinesTitleView.closedImage)
private let identification: Identification
@Published var selectedTimeline: Timeline {
didSet { applyTimelineSelection() }
}
init(timelines: [Timeline], identification: Identification) {
self.timelines = timelines
self.identification = identification
guard let timeline = timelines.first else {
fatalError("TimelinesTitleView must be initialized with a non-empty timelines array")
}
selectedTimeline = timeline
super.init(frame: .zero)
accessibilityTraits = .button
isAccessibilityElement = true
showsMenuAsPrimaryAction = true
isContextMenuInteractionEnabled = true
addSubview(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFit
imageView.setContentHuggingPriority(.required, for: .horizontal)
imageView.tintColor = .label
addSubview(chevronImageView)
chevronImageView.translatesAutoresizingMaskIntoConstraints = false
chevronImageView.contentMode = .scaleAspectFit
chevronImageView.setContentHuggingPriority(.required, for: .horizontal)
addSubview(titleLabel)
titleLabel.translatesAutoresizingMaskIntoConstraints = false
titleLabel.adjustsFontForContentSizeCategory = true
titleLabel.font = .preferredFont(forTextStyle: .headline)
titleLabel.adjustsFontSizeToFitWidth = true
titleLabel.minimumScaleFactor = 0.5
titleLabel.setContentHuggingPriority(.required, for: .horizontal)
titleLabel.setContentHuggingPriority(.required, for: .vertical)
titleLabel.setContentCompressionResistancePriority(.required, for: .vertical)
addSubview(subtitleLabel)
subtitleLabel.translatesAutoresizingMaskIntoConstraints = false
subtitleLabel.adjustsFontForContentSizeCategory = true
subtitleLabel.font = .preferredFont(forTextStyle: .caption2)
subtitleLabel.adjustsFontSizeToFitWidth = true
subtitleLabel.textAlignment = .center
subtitleLabel.minimumScaleFactor = 0.5
subtitleLabel.textColor = .secondaryLabel
subtitleLabel.setContentHuggingPriority(.required, for: .vertical)
subtitleLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
subtitleLabel.setContentCompressionResistancePriority(.justBelowMax, for: .vertical)
NSLayoutConstraint.activate([
imageView.leadingAnchor.constraint(greaterThanOrEqualTo: leadingAnchor),
imageView.topAnchor.constraint(equalTo: titleLabel.topAnchor),
imageView.bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor),
titleLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
titleLabel.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, constant: .compactSpacing),
titleLabel.topAnchor.constraint(equalTo: topAnchor),
chevronImageView.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: .defaultSpacing),
chevronImageView.topAnchor.constraint(equalTo: titleLabel.topAnchor),
chevronImageView.bottomAnchor.constraint(equalTo: titleLabel.bottomAnchor),
chevronImageView.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor),
subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor),
subtitleLabel.leadingAnchor.constraint(equalTo: leadingAnchor),
subtitleLabel.trailingAnchor.constraint(equalTo: trailingAnchor),
subtitleLabel.bottomAnchor.constraint(equalTo: bottomAnchor)
])
applyTimelineSelection()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override var isHighlighted: Bool {
didSet {
alpha = isHighlighted ? Self.highlightedAlpha : 1
}
}
override func menuAttachmentPoint(for configuration: UIContextMenuConfiguration) -> CGPoint {
CGPoint(x: (bounds.width - .systemMenuWidth) / 2 + .systemMenuInset, y: bounds.maxY + .compactSpacing)
}
override func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { [weak self] _ in
guard let self = self else { return nil }
return UIMenu(children: self.timelines.map { timeline in
UIAction(
title: timeline.title,
image: UIImage(systemName: timeline.systemImageName),
attributes: timeline == self.selectedTimeline ? .disabled : [],
state: timeline == self.selectedTimeline ? .on : .off) { _ in
self.selectedTimeline = timeline
}
})
}
}
override func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
willDisplayMenuFor configuration: UIContextMenuConfiguration,
animator: UIContextMenuInteractionAnimating?) {
chevronImageView.image = Self.openImage
}
override func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
willEndFor configuration: UIContextMenuConfiguration,
animator: UIContextMenuInteractionAnimating?) {
chevronImageView.image = Self.closedImage
alpha = 1 // system bug
}
}
private extension TimelinesTitleView {
static let highlightedAlpha: CGFloat = 0.5
static let openImage = UIImage(
systemName: "chevron.compact.up",
withConfiguration: UIImage.SymbolConfiguration(scale: .small))
static let closedImage = UIImage(
systemName: "chevron.compact.down",
withConfiguration: UIImage.SymbolConfiguration(scale: .small))
func applyTimelineSelection() {
imageView.image = UIImage(
systemName: selectedTimeline.systemImageName,
withConfiguration: UIImage.SymbolConfiguration(scale: .small))
titleLabel.text = selectedTimeline.title
subtitleLabel.text = selectedTimeline.subtitle(identification: identification)
}
}

View file

@ -11,8 +11,10 @@ extension CGFloat {
static let hairline = 1 / UIScreen.main.scale
static let minimumButtonDimension: Self = 44
static let barButtonItemDimension: Self = 28
static let newStatusButtonDimension: CGFloat = 54
static let defaultShadowRadius: CGFloat = 2
static let newStatusButtonDimension: Self = 54
static let defaultShadowRadius: Self = 2
static let systemMenuWidth: Self = 250
static let systemMenuInset: Self = 15
}
extension CGRect {