mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-21 15:50:59 +00:00
Re-write stuff in UIKit
This commit is contained in:
parent
02747215c5
commit
2389e1b25c
15 changed files with 424 additions and 640 deletions
40
Extensions/Timeline+Extensions.swift
Normal file
40
Extensions/Timeline+Extensions.swift
Normal file
|
@ -0,0 +1,40 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import ViewModels
|
||||
|
||||
extension Timeline {
|
||||
var title: String {
|
||||
switch self {
|
||||
case .home:
|
||||
return NSLocalizedString("timelines.home", comment: "")
|
||||
case .local:
|
||||
return NSLocalizedString("timelines.local", comment: "")
|
||||
case .federated:
|
||||
return NSLocalizedString("timelines.federated", comment: "")
|
||||
case let .list(list):
|
||||
return list.title
|
||||
case let .tag(tag):
|
||||
return "#".appending(tag)
|
||||
case .profile:
|
||||
return ""
|
||||
case .favorites:
|
||||
return NSLocalizedString("favorites", comment: "")
|
||||
case .bookmarks:
|
||||
return NSLocalizedString("bookmarks", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
var systemImageName: String {
|
||||
switch self {
|
||||
case .home: return "house"
|
||||
case .local: return "building.2.crop.circle"
|
||||
case .federated: return "network"
|
||||
case .list: return "scroll"
|
||||
case .tag: return "number"
|
||||
case .profile: return "person"
|
||||
case .favorites: return "star"
|
||||
case .bookmarks: return "bookmark"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -205,8 +205,5 @@
|
|||
"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 %@";
|
||||
|
|
|
@ -27,7 +27,9 @@
|
|||
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 */; };
|
||||
D035F8A925B9155900DC75ED /* NewStatusButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F8A825B9155900DC75ED /* NewStatusButtonView.swift */; };
|
||||
D035F8B325B9616000DC75ED /* Timeline+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F8B225B9616000DC75ED /* Timeline+Extensions.swift */; };
|
||||
D035F8C725B96A4000DC75ED /* SecondaryNavigationButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D035F8C625B96A4000DC75ED /* SecondaryNavigationButton.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 */; };
|
||||
|
@ -117,7 +119,6 @@
|
|||
D0C7D49D24F7616A001EBDBB /* PostingReadingPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42824F76169001EBDBB /* PostingReadingPreferencesView.swift */; };
|
||||
D0C7D49E24F7616A001EBDBB /* SecondaryNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42924F76169001EBDBB /* SecondaryNavigationView.swift */; };
|
||||
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */; };
|
||||
D0C7D4A324F7616A001EBDBB /* TabNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D42E24F76169001EBDBB /* TabNavigationView.swift */; };
|
||||
D0C7D4C224F7616A001EBDBB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D0C7D45224F76169001EBDBB /* Assets.xcassets */; };
|
||||
D0C7D4C324F7616A001EBDBB /* MetatextApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D45424F76169001EBDBB /* MetatextApp.swift */; };
|
||||
D0C7D4C424F7616A001EBDBB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0C7D45524F76169001EBDBB /* AppDelegate.swift */; };
|
||||
|
@ -213,7 +214,9 @@
|
|||
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>"; };
|
||||
D035F8A825B9155900DC75ED /* NewStatusButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewStatusButtonView.swift; sourceTree = "<group>"; };
|
||||
D035F8B225B9616000DC75ED /* Timeline+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timeline+Extensions.swift"; sourceTree = "<group>"; };
|
||||
D035F8C625B96A4000DC75ED /* SecondaryNavigationButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondaryNavigationButton.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>"; };
|
||||
|
@ -290,7 +293,6 @@
|
|||
D0C7D42824F76169001EBDBB /* PostingReadingPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PostingReadingPreferencesView.swift; sourceTree = "<group>"; };
|
||||
D0C7D42924F76169001EBDBB /* SecondaryNavigationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecondaryNavigationView.swift; sourceTree = "<group>"; };
|
||||
D0C7D42D24F76169001EBDBB /* NotificationTypesPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotificationTypesPreferencesView.swift; sourceTree = "<group>"; };
|
||||
D0C7D42E24F76169001EBDBB /* TabNavigationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabNavigationView.swift; sourceTree = "<group>"; };
|
||||
D0C7D45224F76169001EBDBB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
D0C7D45424F76169001EBDBB /* MetatextApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MetatextApp.swift; sourceTree = "<group>"; };
|
||||
D0C7D45524F76169001EBDBB /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
|
@ -517,6 +519,7 @@
|
|||
D035F86E25B7F30E00DC75ED /* MainNavigationView.swift */,
|
||||
D05936FE25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift */,
|
||||
D03B1B29253818F3008F964B /* MediaPreferencesView.swift */,
|
||||
D035F8A825B9155900DC75ED /* NewStatusButtonView.swift */,
|
||||
D0FCC10F259C4F20000B67DF /* NewStatusView.swift */,
|
||||
D036AA0B254B612B009094DF /* NotificationContentConfiguration.swift */,
|
||||
D036AA01254B6101009094DF /* NotificationListCell.swift */,
|
||||
|
@ -532,12 +535,11 @@
|
|||
D0DD50CA256B1F24004A04F7 /* ReportView.swift */,
|
||||
D0C7D42724F76169001EBDBB /* RootView.swift */,
|
||||
D02E1F94250B13210071AD56 /* SafariView.swift */,
|
||||
D035F8C625B96A4000DC75ED /* SecondaryNavigationButton.swift */,
|
||||
D0C7D42924F76169001EBDBB /* SecondaryNavigationView.swift */,
|
||||
D04226FC2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift */,
|
||||
D0625E55250F086B00502611 /* Status */,
|
||||
D0C7D42524F76169001EBDBB /* TableView.swift */,
|
||||
D0C7D42E24F76169001EBDBB /* TabNavigationView.swift */,
|
||||
D035F89025B8067100DC75ED /* TimelinesTitleView.swift */,
|
||||
D01F41D624F880C400D55A2D /* TouchFallthroughTextView.swift */,
|
||||
D0EA59472522B8B600804347 /* ViewConstants.swift */,
|
||||
D0F2D54A2581CF7D00986197 /* VisualEffectBlur.swift */,
|
||||
|
@ -601,6 +603,7 @@
|
|||
D0E7AD3825870B13005F5E2D /* UIVIewController+Extensions.swift */,
|
||||
D0030981250C6C8500EACB32 /* URL+Extensions.swift */,
|
||||
D0C7D46F24F76169001EBDBB /* View+Extensions.swift */,
|
||||
D035F8B225B9616000DC75ED /* Timeline+Extensions.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
|
@ -825,7 +828,6 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D0C7D4A324F7616A001EBDBB /* TabNavigationView.swift in Sources */,
|
||||
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */,
|
||||
D07EC7CF25B13921006DF726 /* PickerEmoji+Extensions.swift in Sources */,
|
||||
D00702292555E51200F38136 /* ConversationListCell.swift in Sources */,
|
||||
|
@ -849,6 +851,7 @@
|
|||
D08B8D622540DE3B00B1EBEF /* ZoomTransitionController.swift in Sources */,
|
||||
D0F0B12E251A97E400942152 /* TableViewController.swift in Sources */,
|
||||
D0DD50CB256B1F24004A04F7 /* ReportView.swift in Sources */,
|
||||
D035F8B325B9616000DC75ED /* Timeline+Extensions.swift in Sources */,
|
||||
D0FE1C8F253686F9003EF1EB /* PlayerView.swift in Sources */,
|
||||
D0CE9F87258B076900E3A6B6 /* AttachmentUploadView.swift in Sources */,
|
||||
D0F0B113251A86A000942152 /* AccountContentConfiguration.swift in Sources */,
|
||||
|
@ -884,18 +887,19 @@
|
|||
D01C6FAC252024BD003D0300 /* Array+Extensions.swift in Sources */,
|
||||
D0C7D4D924F7616A001EBDBB /* KingfisherOptionsInfo+Extensions.swift in Sources */,
|
||||
D08B8D72254246E200B1EBEF /* PollView.swift in Sources */,
|
||||
D035F8A925B9155900DC75ED /* NewStatusButtonView.swift in Sources */,
|
||||
D0EA59402522AC8700804347 /* CardView.swift in Sources */,
|
||||
D0F0B10E251A868200942152 /* AccountView.swift in Sources */,
|
||||
D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */,
|
||||
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 */,
|
||||
D035F8C725B96A4000DC75ED /* SecondaryNavigationButton.swift in Sources */,
|
||||
D0EA59482522B8B600804347 /* ViewConstants.swift in Sources */,
|
||||
D04226FD2546AC0B000980A3 /* StartupAndSyncingPreferencesView.swift in Sources */,
|
||||
D036AA0C254B612B009094DF /* NotificationContentConfiguration.swift in Sources */,
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import Combine
|
||||
import SwiftUI
|
||||
import ViewModels
|
||||
|
||||
final class MainNavigationViewController: UITabBarController {
|
||||
private let viewModel: NavigationViewModel
|
||||
private let rootViewModel: RootViewModel
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
private weak var presentedSecondaryNavigation: UINavigationController?
|
||||
|
||||
init(viewModel: NavigationViewModel, rootViewModel: RootViewModel) {
|
||||
self.viewModel = viewModel
|
||||
|
@ -22,13 +25,40 @@ final class MainNavigationViewController: UITabBarController {
|
|||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
let timelinesViewController = TimelinesViewController(
|
||||
viewModel: viewModel,
|
||||
rootViewModel: rootViewModel)
|
||||
let timelinesNavigationController = UINavigationController(rootViewController: timelinesViewController)
|
||||
setupViewControllers()
|
||||
|
||||
if let notificationsViewModel = viewModel.notificationsViewModel,
|
||||
let conversationsViewModel = viewModel.conversationsViewModel {
|
||||
if viewModel.identification.identity.authenticated {
|
||||
setupNewStatusButton()
|
||||
}
|
||||
|
||||
viewModel.$presentingSecondaryNavigation.sink { [weak self] in
|
||||
if $0 {
|
||||
self?.presentSecondaryNavigation()
|
||||
} else {
|
||||
self?.dismissSecondaryNavigation()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
viewModel.timelineNavigations
|
||||
.sink { [weak self] _ in self?.selectedIndex = 0 }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
viewModel.refreshIdentity()
|
||||
}
|
||||
}
|
||||
|
||||
private extension MainNavigationViewController {
|
||||
func setupViewControllers() {
|
||||
var controllers: [UIViewController] = [TimelinesViewController(
|
||||
viewModel: viewModel,
|
||||
rootViewModel: rootViewModel)]
|
||||
|
||||
if let notificationsViewModel = viewModel.notificationsViewModel {
|
||||
let notificationsViewController = TableViewController(
|
||||
viewModel: notificationsViewModel,
|
||||
rootViewModel: rootViewModel,
|
||||
|
@ -36,9 +66,10 @@ final class MainNavigationViewController: UITabBarController {
|
|||
|
||||
notificationsViewController.tabBarItem = NavigationViewModel.Tab.notifications.tabBarItem
|
||||
|
||||
let notificationsNavigationViewController = UINavigationController(
|
||||
rootViewController: notificationsViewController)
|
||||
controllers.append(notificationsViewController)
|
||||
}
|
||||
|
||||
if let conversationsViewModel = viewModel.conversationsViewModel {
|
||||
let conversationsViewController = TableViewController(
|
||||
viewModel: conversationsViewModel,
|
||||
rootViewModel: rootViewModel,
|
||||
|
@ -47,18 +78,64 @@ final class MainNavigationViewController: UITabBarController {
|
|||
conversationsViewController.tabBarItem = NavigationViewModel.Tab.messages.tabBarItem
|
||||
conversationsViewController.navigationItem.title = NavigationViewModel.Tab.messages.title
|
||||
|
||||
let conversationsNavigationViewController = UINavigationController(
|
||||
rootViewController: conversationsViewController)
|
||||
controllers.append(conversationsViewController)
|
||||
}
|
||||
|
||||
viewControllers = [
|
||||
timelinesNavigationController,
|
||||
notificationsNavigationViewController,
|
||||
conversationsNavigationViewController
|
||||
]
|
||||
} else {
|
||||
viewControllers = [
|
||||
timelinesNavigationController
|
||||
]
|
||||
let secondaryNavigationButton = SecondaryNavigationButton(viewModel: viewModel, rootViewModel: rootViewModel)
|
||||
|
||||
for controller in controllers {
|
||||
controller.navigationItem.leftBarButtonItem = secondaryNavigationButton
|
||||
}
|
||||
|
||||
viewControllers = controllers.map(UINavigationController.init(rootViewController:))
|
||||
}
|
||||
|
||||
func setupNewStatusButton() {
|
||||
let newStatusButtonView = NewStatusButtonView(primaryAction: UIAction { [weak self] _ in
|
||||
guard let self = self else { return }
|
||||
let newStatusViewModel = self.rootViewModel.newStatusViewModel(
|
||||
identification: self.viewModel.identification)
|
||||
let newStatusViewController = NewStatusViewController(viewModel: newStatusViewModel)
|
||||
let newStatusNavigationController = UINavigationController(rootViewController: newStatusViewController)
|
||||
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
newStatusNavigationController.modalPresentationStyle = .overFullScreen
|
||||
}
|
||||
|
||||
self.present(newStatusNavigationController, animated: true)
|
||||
})
|
||||
|
||||
view.addSubview(newStatusButtonView)
|
||||
newStatusButtonView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
newStatusButtonView.widthAnchor.constraint(equalToConstant: .newStatusButtonDimension),
|
||||
newStatusButtonView.heightAnchor.constraint(equalToConstant: .newStatusButtonDimension),
|
||||
newStatusButtonView.trailingAnchor.constraint(
|
||||
equalTo: view.safeAreaLayoutGuide.trailingAnchor,
|
||||
constant: -.defaultSpacing * 2),
|
||||
newStatusButtonView.bottomAnchor.constraint(equalTo: tabBar.topAnchor, constant: -.defaultSpacing * 2)
|
||||
])
|
||||
}
|
||||
|
||||
func presentSecondaryNavigation() {
|
||||
let secondaryNavigationView = SecondaryNavigationView(viewModel: viewModel)
|
||||
.environmentObject(rootViewModel)
|
||||
let hostingController = UIHostingController(rootView: secondaryNavigationView)
|
||||
|
||||
hostingController.navigationItem.leftBarButtonItem = UIBarButtonItem(
|
||||
systemItem: .close,
|
||||
primaryAction: UIAction { [weak self] _ in self?.viewModel.presentingSecondaryNavigation = false })
|
||||
|
||||
let navigationController = UINavigationController(rootViewController: hostingController)
|
||||
|
||||
presentedSecondaryNavigation = navigationController
|
||||
present(navigationController, animated: true)
|
||||
}
|
||||
|
||||
func dismissSecondaryNavigation() {
|
||||
if presentedViewController == presentedSecondaryNavigation {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -66,6 +66,8 @@ final class NewStatusViewController: UIViewController {
|
|||
activityIndicatorView.centerYAnchor.constraint(equalTo: scrollView.centerYAnchor)
|
||||
])
|
||||
|
||||
setupBarButtonItems(identification: viewModel.identification)
|
||||
|
||||
postButton.primaryAction = UIAction(title: NSLocalizedString("post", comment: "")) { [weak self] _ in
|
||||
self?.viewModel.post()
|
||||
}
|
||||
|
@ -84,12 +86,6 @@ final class NewStatusViewController: UIViewController {
|
|||
|
||||
setupViewModelBindings()
|
||||
}
|
||||
|
||||
override func didMove(toParent parent: UIViewController?) {
|
||||
super.didMove(toParent: parent)
|
||||
|
||||
setupBarButtonItems(identification: viewModel.identification)
|
||||
}
|
||||
}
|
||||
|
||||
extension NewStatusViewController: PHPickerViewControllerDelegate {
|
||||
|
@ -251,15 +247,15 @@ private extension NewStatusViewController {
|
|||
}
|
||||
|
||||
func setupBarButtonItems(identification: Identification) {
|
||||
let closeButton = UIBarButtonItem(
|
||||
systemItem: .close,
|
||||
let cancelButton = UIBarButtonItem(
|
||||
systemItem: .cancel,
|
||||
primaryAction: UIAction { [weak self] _ in self?.dismiss() })
|
||||
|
||||
parent?.navigationItem.leftBarButtonItem = closeButton
|
||||
parent?.navigationItem.titleView = viewModel.canChangeIdentity
|
||||
navigationItem.leftBarButtonItem = cancelButton
|
||||
navigationItem.titleView = viewModel.canChangeIdentity
|
||||
? changeIdentityButton(identification: identification)
|
||||
: nil
|
||||
parent?.navigationItem.rightBarButtonItem = postButton
|
||||
navigationItem.rightBarButtonItem = postButton
|
||||
}
|
||||
|
||||
func presentMediaPicker(compositionViewModel: CompositionViewModel) {
|
||||
|
|
|
@ -5,7 +5,7 @@ import UIKit
|
|||
import ViewModels
|
||||
|
||||
final class TimelinesViewController: UIPageViewController {
|
||||
private let titleView: TimelinesTitleView
|
||||
private let segmentedControl = UISegmentedControl()
|
||||
private let timelineViewControllers: [TableViewController]
|
||||
private let viewModel: NavigationViewModel
|
||||
private let rootViewModel: RootViewModel
|
||||
|
@ -15,31 +15,18 @@ final class TimelinesViewController: UIPageViewController {
|
|||
self.viewModel = viewModel
|
||||
self.rootViewModel = rootViewModel
|
||||
|
||||
let timelineViewModels: [CollectionViewModel]
|
||||
var timelineViewControllers = [TableViewController]()
|
||||
|
||||
if let homeTimelineViewModel = viewModel.homeTimelineViewModel {
|
||||
timelineViewModels = [
|
||||
homeTimelineViewModel,
|
||||
viewModel.localTimelineViewModel,
|
||||
viewModel.federatedTimelineViewModel]
|
||||
} else {
|
||||
timelineViewModels = [
|
||||
viewModel.localTimelineViewModel,
|
||||
viewModel.federatedTimelineViewModel]
|
||||
for (index, timeline) in viewModel.timelines.enumerated() {
|
||||
timelineViewControllers.append(
|
||||
TableViewController(
|
||||
viewModel: viewModel.viewModel(timeline: timeline),
|
||||
rootViewModel: rootViewModel,
|
||||
identification: viewModel.identification))
|
||||
segmentedControl.insertSegment(withTitle: timeline.title, at: index, animated: false)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
self.timelineViewControllers = timelineViewControllers
|
||||
|
||||
super.init(transitionStyle: .scroll,
|
||||
navigationOrientation: .horizontal,
|
||||
|
@ -66,24 +53,35 @@ final class TimelinesViewController: UIPageViewController {
|
|||
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
|
||||
navigationItem.titleView = segmentedControl
|
||||
segmentedControl.selectedSegmentIndex = 0
|
||||
segmentedControl.addAction(
|
||||
UIAction { [weak self] _ in
|
||||
guard let self = self,
|
||||
let currentViewController = self.viewControllers?.first as? TableViewController,
|
||||
let currentIndex = self.timelineViewControllers.firstIndex(of: currentViewController),
|
||||
index != currentIndex
|
||||
self.segmentedControl.selectedSegmentIndex != currentIndex
|
||||
else { return }
|
||||
|
||||
self.setViewControllers(
|
||||
[self.timelineViewControllers[index]],
|
||||
direction: index > currentIndex ? .forward : .reverse,
|
||||
[self.timelineViewControllers[self.segmentedControl.selectedSegmentIndex]],
|
||||
direction: self.segmentedControl.selectedSegmentIndex > currentIndex ? .forward : .reverse,
|
||||
animated: !UIAccessibility.isReduceMotionEnabled)
|
||||
},
|
||||
for: .valueChanged)
|
||||
|
||||
viewModel.timelineNavigations.sink { [weak self] in
|
||||
guard let self = self else { return }
|
||||
|
||||
let vc = TableViewController(
|
||||
viewModel: self.viewModel.viewModel(timeline: $0),
|
||||
rootViewModel: self.rootViewModel,
|
||||
identification: self.viewModel.identification)
|
||||
|
||||
vc.navigationItem.title = $0.title
|
||||
|
||||
self.show(vc, sender: self)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
@ -122,10 +120,6 @@ extension TimelinesViewController: UIPageViewControllerDelegate {
|
|||
let index = timelineViewControllers.firstIndex(of: viewController)
|
||||
else { return }
|
||||
|
||||
let timeline = titleView.timelines[index]
|
||||
|
||||
if titleView.selectedTimeline != timeline {
|
||||
titleView.selectedTimeline = timeline
|
||||
}
|
||||
segmentedControl.selectedSegmentIndex = index
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,41 +7,11 @@ import ServiceLayer
|
|||
|
||||
public final class NavigationViewModel: ObservableObject {
|
||||
public let identification: Identification
|
||||
public let timelineNavigations: AnyPublisher<Timeline, Never>
|
||||
|
||||
@Published public private(set) var recentIdentities = [Identity]()
|
||||
@Published public var timeline: Timeline {
|
||||
didSet {
|
||||
timelineViewModel = CollectionItemsViewModel(
|
||||
collectionService: identification.service.service(timeline: timeline),
|
||||
identification: identification)
|
||||
}
|
||||
}
|
||||
@Published public private(set) var timelinesAndLists: [Timeline]
|
||||
@Published public var presentingSecondaryNavigation = false
|
||||
@Published public var presentingNewStatus = false
|
||||
@Published public var alertItem: AlertItem?
|
||||
public private(set) var timelineViewModel: CollectionItemsViewModel
|
||||
|
||||
public lazy var homeTimelineViewModel: CollectionViewModel? = {
|
||||
if identification.identity.authenticated {
|
||||
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 {
|
||||
|
@ -71,18 +41,12 @@ public final class NavigationViewModel: ObservableObject {
|
|||
}
|
||||
}()
|
||||
|
||||
private let timelineNavigationsSubject = PassthroughSubject<Timeline, Never>()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
public init(identification: Identification) {
|
||||
self.identification = identification
|
||||
let timeline: Timeline = identification.identity.authenticated ? .home : .local
|
||||
self.timeline = timeline
|
||||
timelineViewModel = CollectionItemsViewModel(
|
||||
collectionService: identification.service.service(timeline: timeline),
|
||||
identification: identification)
|
||||
timelinesAndLists = identification.identity.authenticated
|
||||
? Timeline.authenticatedDefaults
|
||||
: Timeline.unauthenticatedDefaults
|
||||
timelineNavigations = timelineNavigationsSubject.eraseToAnyPublisher()
|
||||
|
||||
identification.$identity
|
||||
.sink { [weak self] _ in self?.objectWillChange.send() }
|
||||
|
@ -91,17 +55,17 @@ public final class NavigationViewModel: ObservableObject {
|
|||
identification.service.recentIdentitiesPublisher()
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.assign(to: &$recentIdentities)
|
||||
|
||||
if identification.identity.authenticated {
|
||||
identification.service.listsPublisher()
|
||||
.map { Timeline.authenticatedDefaults + $0 }
|
||||
.assignErrorsToAlertItem(to: \.alertItem, on: self)
|
||||
.assign(to: &$timelinesAndLists)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension NavigationViewModel {
|
||||
enum Tab: CaseIterable {
|
||||
case timelines
|
||||
case explore
|
||||
case notifications
|
||||
case messages
|
||||
}
|
||||
|
||||
var tabs: [Tab] {
|
||||
if identification.identity.authenticated {
|
||||
return Tab.allCases
|
||||
|
@ -110,12 +74,11 @@ public extension NavigationViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
var timelineSubtitle: String {
|
||||
switch timeline {
|
||||
case .home, .favorites, .bookmarks, .list:
|
||||
return identification.identity.handle
|
||||
case .local, .federated, .tag, .profile:
|
||||
return identification.identity.instance?.uri ?? ""
|
||||
var timelines: [Timeline] {
|
||||
if identification.identity.authenticated {
|
||||
return Timeline.authenticatedDefaults
|
||||
} else {
|
||||
return Timeline.unauthenticatedDefaults
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -156,29 +119,15 @@ public extension NavigationViewModel {
|
|||
.sink { _ in } receiveValue: { _ in }
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
public extension NavigationViewModel {
|
||||
enum Tab: CaseIterable {
|
||||
case timelines
|
||||
case explore
|
||||
case notifications
|
||||
case messages
|
||||
func navigate(timeline: Timeline) {
|
||||
presentingSecondaryNavigation = false
|
||||
timelineNavigationsSubject.send(timeline)
|
||||
}
|
||||
|
||||
func favoritesViewModel() -> CollectionViewModel {
|
||||
func viewModel(timeline: Timeline) -> CollectionItemsViewModel {
|
||||
CollectionItemsViewModel(
|
||||
collectionService: identification.service.service(timeline: .favorites),
|
||||
identification: identification)
|
||||
}
|
||||
|
||||
func bookmarksViewModel() -> CollectionViewModel {
|
||||
CollectionItemsViewModel(
|
||||
collectionService: identification.service.service(timeline: .bookmarks),
|
||||
collectionService: identification.service.service(timeline: timeline),
|
||||
identification: identification)
|
||||
}
|
||||
}
|
||||
|
||||
extension NavigationViewModel.Tab: Identifiable {
|
||||
public var id: Self { self }
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Mastodon
|
||||
import SwiftUI
|
||||
import ViewModels
|
||||
|
||||
|
@ -27,9 +28,11 @@ struct ListsView: View {
|
|||
}
|
||||
Section {
|
||||
ForEach(viewModel.lists) { list in
|
||||
Button(list.title) {
|
||||
rootViewModel.navigationViewModel?.timeline = .list(list)
|
||||
rootViewModel.navigationViewModel?.presentingSecondaryNavigation = false
|
||||
Button {
|
||||
rootViewModel.navigationViewModel?.navigate(timeline: .list(list))
|
||||
} label: {
|
||||
Text(list.title)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
}
|
||||
.onDelete {
|
||||
|
|
83
Views/NewStatusButtonView.swift
Normal file
83
Views/NewStatusButtonView.swift
Normal file
|
@ -0,0 +1,83 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
|
||||
final class NewStatusButtonView: UIView {
|
||||
let button: UIButton
|
||||
|
||||
init(primaryAction: UIAction) {
|
||||
button = UIButton(type: .custom, primaryAction: primaryAction)
|
||||
|
||||
super.init(frame: .zero)
|
||||
|
||||
initialSetup()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
private extension NewStatusButtonView {
|
||||
// swiftlint:disable:next function_body_length
|
||||
func initialSetup() {
|
||||
let blurEffect = UIBlurEffect(style: .systemChromeMaterial)
|
||||
let blurView = UIVisualEffectView(effect: blurEffect)
|
||||
let vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: blurEffect, style: .label))
|
||||
|
||||
backgroundColor = .clear
|
||||
layer.cornerRadius = .newStatusButtonDimension / 2
|
||||
layer.shadowPath = UIBezierPath(
|
||||
ovalIn: .init(
|
||||
origin: .zero,
|
||||
size: .init(
|
||||
width: .newStatusButtonDimension,
|
||||
height: .newStatusButtonDimension)))
|
||||
.cgPath
|
||||
layer.shadowOffset = .zero
|
||||
layer.shadowRadius = .defaultShadowRadius
|
||||
layer.shadowOpacity = 0.25
|
||||
|
||||
addSubview(blurView)
|
||||
blurView.translatesAutoresizingMaskIntoConstraints = false
|
||||
blurView.layer.cornerRadius = .newStatusButtonDimension / 2
|
||||
blurView.clipsToBounds = true
|
||||
blurView.contentView.addSubview(vibrancyView)
|
||||
vibrancyView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let touchStartAction = UIAction { [weak self] _ in self?.alpha = 0.75 }
|
||||
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.addAction(touchStartAction, for: .touchDown)
|
||||
button.addAction(touchStartAction, for: .touchDragEnter)
|
||||
|
||||
let touchEndAction = UIAction { [weak self] _ in self?.alpha = 1 }
|
||||
|
||||
button.addAction(touchEndAction, for: .touchDragExit)
|
||||
button.addAction(touchEndAction, for: .touchUpInside)
|
||||
button.addAction(touchEndAction, for: .touchUpOutside)
|
||||
button.addAction(touchEndAction, for: .touchCancel)
|
||||
|
||||
button.setImage(
|
||||
UIImage(systemName: "pencil",
|
||||
withConfiguration: UIImage.SymbolConfiguration(pointSize: .newStatusButtonDimension / 2)),
|
||||
for: .normal)
|
||||
vibrancyView.contentView.addSubview(button)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
blurView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
blurView.topAnchor.constraint(equalTo: topAnchor),
|
||||
blurView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
blurView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||
vibrancyView.leadingAnchor.constraint(equalTo: blurView.contentView.leadingAnchor),
|
||||
vibrancyView.topAnchor.constraint(equalTo: blurView.contentView.topAnchor),
|
||||
vibrancyView.trailingAnchor.constraint(equalTo: blurView.contentView.trailingAnchor),
|
||||
vibrancyView.bottomAnchor.constraint(equalTo: blurView.contentView.bottomAnchor),
|
||||
button.leadingAnchor.constraint(equalTo: vibrancyView.contentView.leadingAnchor),
|
||||
button.topAnchor.constraint(equalTo: vibrancyView.contentView.topAnchor),
|
||||
button.trailingAnchor.constraint(equalTo: vibrancyView.contentView.trailingAnchor),
|
||||
button.bottomAnchor.constraint(equalTo: vibrancyView.contentView.bottomAnchor)
|
||||
])
|
||||
}
|
||||
}
|
69
Views/SecondaryNavigationButton.swift
Normal file
69
Views/SecondaryNavigationButton.swift
Normal file
|
@ -0,0 +1,69 @@
|
|||
// Copyright © 2021 Metabolist. All rights reserved.
|
||||
|
||||
import Combine
|
||||
import Kingfisher
|
||||
import UIKit
|
||||
import ViewModels
|
||||
|
||||
final class SecondaryNavigationButton: UIBarButtonItem {
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init(viewModel: NavigationViewModel, rootViewModel: RootViewModel) {
|
||||
super.init()
|
||||
|
||||
let button = UIButton(
|
||||
type: .custom,
|
||||
primaryAction: UIAction { _ in viewModel.presentingSecondaryNavigation = true })
|
||||
let downsampled = KingfisherOptionsInfo.downsampled(
|
||||
dimension: .barButtonItemDimension,
|
||||
scaleFactor: UIScreen.main.scale)
|
||||
|
||||
button.imageView?.contentMode = .scaleAspectFill
|
||||
button.layer.cornerRadius = .barButtonItemDimension / 2
|
||||
button.clipsToBounds = true
|
||||
|
||||
customView = button
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
button.widthAnchor.constraint(equalToConstant: .barButtonItemDimension),
|
||||
button.heightAnchor.constraint(equalToConstant: .barButtonItemDimension)
|
||||
])
|
||||
|
||||
viewModel.identification.$identity.sink {
|
||||
button.kf.setImage(
|
||||
with: $0.image,
|
||||
for: .normal,
|
||||
placeholder: UIImage(systemName: "line.horizontal.3"),
|
||||
options: downsampled)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
viewModel.$recentIdentities.sink { identities in
|
||||
button.menu = UIMenu(children: identities.map { identity in
|
||||
UIDeferredMenuElement { completion in
|
||||
let action = UIAction(title: identity.handle) { _ in
|
||||
rootViewModel.identitySelected(id: identity.id)
|
||||
}
|
||||
|
||||
if let image = identity.image {
|
||||
KingfisherManager.shared.retrieveImage(with: image, options: downsampled) {
|
||||
if case let .success(value) = $0 {
|
||||
action.image = value.image
|
||||
}
|
||||
|
||||
completion([action])
|
||||
}
|
||||
} else {
|
||||
completion([action])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
|
@ -10,73 +10,76 @@ struct SecondaryNavigationView: View {
|
|||
@Environment(\.displayScale) var displayScale: CGFloat
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
Section {
|
||||
NavigationLink(
|
||||
destination: IdentitiesView(viewModel: .init(identification: viewModel.identification)),
|
||||
label: {
|
||||
HStack {
|
||||
KFImage(viewModel.identification.identity.image)
|
||||
.downsampled(dimension: .avatarDimension, scaleFactor: displayScale)
|
||||
VStack(alignment: .leading) {
|
||||
if viewModel.identification.identity.authenticated {
|
||||
if let account = viewModel.identification.identity.account {
|
||||
CustomEmojiText(
|
||||
text: account.displayName,
|
||||
emojis: account.emojis,
|
||||
textStyle: .headline)
|
||||
}
|
||||
Text(viewModel.identification.identity.handle)
|
||||
Form {
|
||||
Section {
|
||||
NavigationLink(
|
||||
destination: IdentitiesView(viewModel: .init(identification: viewModel.identification))
|
||||
.environmentObject(rootViewModel)
|
||||
.environmentObject(viewModel.identification),
|
||||
label: {
|
||||
HStack {
|
||||
KFImage(viewModel.identification.identity.image)
|
||||
.downsampled(dimension: .avatarDimension, scaleFactor: displayScale)
|
||||
VStack(alignment: .leading) {
|
||||
if viewModel.identification.identity.authenticated {
|
||||
if let account = viewModel.identification.identity.account {
|
||||
CustomEmojiText(
|
||||
text: account.displayName,
|
||||
emojis: account.emojis,
|
||||
textStyle: .headline)
|
||||
}
|
||||
Text(viewModel.identification.identity.handle)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.5)
|
||||
} else {
|
||||
Text(viewModel.identification.identity.handle)
|
||||
.font(.headline)
|
||||
if let instance = viewModel.identification.identity.instance {
|
||||
Text(instance.uri)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.5)
|
||||
} else {
|
||||
Text(viewModel.identification.identity.handle)
|
||||
.font(.headline)
|
||||
if let instance = viewModel.identification.identity.instance {
|
||||
Text(instance.uri)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.5)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
Text("secondary-navigation.manage-accounts")
|
||||
.font(.subheadline)
|
||||
}
|
||||
.padding()
|
||||
|
||||
Spacer()
|
||||
Text("secondary-navigation.manage-accounts")
|
||||
.font(.subheadline)
|
||||
}
|
||||
})
|
||||
.padding()
|
||||
}
|
||||
})
|
||||
}
|
||||
Section {
|
||||
NavigationLink(destination: ListsView(viewModel: .init(identification: viewModel.identification))
|
||||
.environmentObject(rootViewModel)
|
||||
.environmentObject(viewModel.identification)) {
|
||||
Label("secondary-navigation.lists", systemImage: "scroll")
|
||||
}
|
||||
Section {
|
||||
NavigationLink(destination: ListsView(viewModel: .init(identification: viewModel.identification))) {
|
||||
Label("secondary-navigation.lists", systemImage: "scroll")
|
||||
ForEach([Timeline.favorites, Timeline.bookmarks]) { timeline in
|
||||
Button {
|
||||
viewModel.navigate(timeline: timeline)
|
||||
} label: {
|
||||
Label {
|
||||
Text(timeline.title).foregroundColor(.primary)
|
||||
} icon: {
|
||||
Image(systemName: timeline.systemImageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
Section {
|
||||
NavigationLink(
|
||||
"secondary-navigation.preferences",
|
||||
destination: PreferencesView(
|
||||
viewModel: .init(identification: viewModel.identification)))
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button {
|
||||
viewModel.presentingSecondaryNavigation = false
|
||||
} label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
}
|
||||
Section {
|
||||
NavigationLink(
|
||||
destination: PreferencesView(viewModel: .init(identification: viewModel.identification))
|
||||
.environmentObject(rootViewModel)
|
||||
.environmentObject(viewModel.identification)) {
|
||||
Label("secondary-navigation.preferences", systemImage: "gear")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.environmentObject(viewModel.identification)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,268 +0,0 @@
|
|||
// Copyright © 2020 Metabolist. All rights reserved.
|
||||
|
||||
import Kingfisher
|
||||
import SwiftUI
|
||||
import ViewModels
|
||||
|
||||
struct TabNavigationView: View {
|
||||
@ObservedObject var viewModel: NavigationViewModel
|
||||
@EnvironmentObject var rootViewModel: RootViewModel
|
||||
@Environment(\.displayScale) var displayScale: CGFloat
|
||||
@State var selectedTab = NavigationViewModel.Tab.timelines
|
||||
|
||||
@State private var contextMenuImages = [UUID: KFImage]()
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
if viewModel.identification.identity.pending {
|
||||
pendingView
|
||||
} else {
|
||||
TabView(selection: $selectedTab) {
|
||||
ForEach(viewModel.tabs) { tab in
|
||||
NavigationView {
|
||||
view(tab: tab)
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.tabItem {
|
||||
Label(tab.title, systemImage: tab.systemImageName)
|
||||
.accessibility(label: Text(tab.title))
|
||||
}
|
||||
.tag(tab)
|
||||
.overlay(newStatusButton, alignment: .bottomTrailing)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.environmentObject(viewModel.identification)
|
||||
.sheet(isPresented: $viewModel.presentingSecondaryNavigation) {
|
||||
SecondaryNavigationView(viewModel: viewModel)
|
||||
.environmentObject(viewModel)
|
||||
.environmentObject(rootViewModel)
|
||||
}
|
||||
.background(
|
||||
EmptyView()
|
||||
.fullScreenCover(isPresented: $viewModel.presentingNewStatus) {
|
||||
NavigationView {
|
||||
NewStatusView { rootViewModel.newStatusViewModel(identification: viewModel.identification) }
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
.environmentObject(viewModel)
|
||||
.environmentObject(rootViewModel)
|
||||
})
|
||||
.alertItem($viewModel.alertItem)
|
||||
.onAppear(perform: viewModel.refreshIdentity)
|
||||
// Have to preload these, otherwise the context menu won't display them when first expanded
|
||||
.onReceive(viewModel.$recentIdentities) {
|
||||
contextMenuImages = Dictionary(uniqueKeysWithValues: $0.map {
|
||||
($0.id, KFImage($0.image)
|
||||
.downsampled(
|
||||
dimension: .barButtonItemDimension,
|
||||
scaleFactor: displayScale)
|
||||
.renderingMode(.original))
|
||||
})
|
||||
}
|
||||
.onReceive(NotificationCenter.default
|
||||
.publisher(for: UIScene.willEnterForegroundNotification)
|
||||
.map { _ in () },
|
||||
perform: viewModel.refreshIdentity)
|
||||
}
|
||||
}
|
||||
|
||||
private extension TabNavigationView {
|
||||
@ViewBuilder
|
||||
var pendingView: some View {
|
||||
NavigationView {
|
||||
Text("pending.pending-confirmation")
|
||||
.navigationBarItems(leading: secondaryNavigationButton)
|
||||
.navigationTitle(viewModel.identification.identity.handle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
.navigationViewStyle(StackNavigationViewStyle())
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
// swiftlint:disable:next function_body_length
|
||||
func view(tab: NavigationViewModel.Tab) -> some View {
|
||||
switch tab {
|
||||
case .timelines:
|
||||
TableView { viewModel.timelineViewModel }
|
||||
.id(viewModel.timeline.id)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.navigationTitle(viewModel.timeline.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
VStack {
|
||||
Text(viewModel.timeline.title)
|
||||
.font(.headline)
|
||||
Text(viewModel.timelineSubtitle)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarItems(
|
||||
leading: secondaryNavigationButton,
|
||||
trailing: Menu {
|
||||
ForEach(viewModel.timelinesAndLists) { timeline in
|
||||
Button {
|
||||
viewModel.timeline = timeline
|
||||
} label: {
|
||||
Label(timeline.title,
|
||||
systemImage: timeline.systemImageName)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: viewModel.timeline.systemImageName)
|
||||
.padding([.leading, .top, .bottom])
|
||||
})
|
||||
case .notifications:
|
||||
if let notificationsViewModel = viewModel.notificationsViewModel {
|
||||
TableView { notificationsViewModel }
|
||||
.id(tab)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.navigationTitle("notifications")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarItems(leading: secondaryNavigationButton)
|
||||
}
|
||||
case .messages:
|
||||
if let conversationsViewModel = viewModel.conversationsViewModel {
|
||||
TableView { conversationsViewModel }
|
||||
.id(tab)
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
.navigationTitle("messages")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarItems(leading: secondaryNavigationButton)
|
||||
}
|
||||
default: Text(tab.title)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var secondaryNavigationButton: some View {
|
||||
Button {
|
||||
viewModel.presentingSecondaryNavigation.toggle()
|
||||
} label: {
|
||||
KFImage(viewModel.identification.identity.image)
|
||||
.downsampled(
|
||||
dimension: .barButtonItemDimension,
|
||||
scaleFactor: displayScale)
|
||||
.placeholder { Image(systemName: "gear") }
|
||||
.renderingMode(.original)
|
||||
.contextMenu(ContextMenu {
|
||||
ForEach(viewModel.recentIdentities) { recentIdentity in
|
||||
Button {
|
||||
rootViewModel.identitySelected(id: recentIdentity.id)
|
||||
} label: {
|
||||
Label(
|
||||
title: { Text(recentIdentity.handle) },
|
||||
icon: { contextMenuImages[recentIdentity.id] })
|
||||
}
|
||||
}
|
||||
})
|
||||
.padding([.trailing, .top, .bottom])
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var newStatusButton: some View {
|
||||
if viewModel.identification.identity.authenticated
|
||||
&& !viewModel.identification.identity.pending {
|
||||
Button {
|
||||
viewModel.presentingNewStatus = true
|
||||
} label: {
|
||||
VisualEffectBlur(vibrancyStyle: .label) {
|
||||
Image(systemName: "pencil")
|
||||
.resizable()
|
||||
.frame(width: .newStatusButtonDimension / 2,
|
||||
height: .newStatusButtonDimension / 2)
|
||||
}
|
||||
.clipShape(Circle())
|
||||
.frame(width: .newStatusButtonDimension,
|
||||
height: .newStatusButtonDimension)
|
||||
.shadow(radius: .defaultShadowRadius)
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: move
|
||||
extension Timeline {
|
||||
var title: String {
|
||||
switch self {
|
||||
case .home:
|
||||
return NSLocalizedString("timelines.home", comment: "")
|
||||
case .local:
|
||||
return NSLocalizedString("timelines.local", comment: "")
|
||||
case .federated:
|
||||
return NSLocalizedString("timelines.federated", comment: "")
|
||||
case let .list(list):
|
||||
return list.title
|
||||
case let .tag(tag):
|
||||
return "#".appending(tag)
|
||||
case .profile:
|
||||
return ""
|
||||
case .favorites:
|
||||
return NSLocalizedString("favorites", comment: "")
|
||||
case .bookmarks:
|
||||
return NSLocalizedString("bookmarks", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
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 "building.2.crop.circle"
|
||||
case .federated: return "network"
|
||||
case .list: return "scroll"
|
||||
case .tag: return "number"
|
||||
case .profile: return "person"
|
||||
case .favorites: return "star"
|
||||
case .bookmarks: return "bookmark"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
import PreviewViewModels
|
||||
|
||||
struct TabNavigation_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TabNavigationView(viewModel: NavigationViewModel(identification: .preview))
|
||||
.environmentObject(Identification.preview)
|
||||
.environmentObject(RootViewModel.preview)
|
||||
}
|
||||
}
|
||||
#endif
|
|
@ -16,13 +16,3 @@ struct TableView: UIViewControllerRepresentable {
|
|||
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
import PreviewViewModels
|
||||
|
||||
struct StatusListView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TableView { NavigationViewModel(identification: .preview).timelineViewModel }
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
@ -1,153 +0,0 @@
|
|||
// 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)
|
||||
}
|
||||
}
|
|
@ -11,7 +11,7 @@ extension CGFloat {
|
|||
static let hairline = 1 / UIScreen.main.scale
|
||||
static let minimumButtonDimension: Self = 44
|
||||
static let barButtonItemDimension: Self = 28
|
||||
static let newStatusButtonDimension: Self = 54
|
||||
static let newStatusButtonDimension: Self = 58
|
||||
static let defaultShadowRadius: Self = 2
|
||||
static let systemMenuWidth: Self = 250
|
||||
static let systemMenuInset: Self = 15
|
||||
|
|
Loading…
Reference in a new issue