From 57b4e67cc2d4621a88ca7a842bfb9e31e272bfa0 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 6 Oct 2018 10:38:36 -0400 Subject: [PATCH] Allow tabs to be enabled/disabled and reordered --- Tusker.xcodeproj/project.pbxproj | 26 ++++- Tusker/Preferences/Preferences.swift | 17 ++- Tusker/Preferences/Tab.swift | 35 ++++++ .../Main/MainTabBarViewController.swift | 66 +++++++----- .../Preferences/Preferences.storyboard | 102 +++++++++++++----- .../Preferences/TabsTableViewController.swift | 92 ++++++++++++++++ Tusker/Views/Tab/TabTableViewCell.swift | 47 ++++++++ Tusker/Views/Tab/TabTableViewCell.xib | 47 ++++++++ 8 files changed, 375 insertions(+), 57 deletions(-) create mode 100644 Tusker/Preferences/Tab.swift create mode 100644 Tusker/Screens/Preferences/TabsTableViewController.swift create mode 100644 Tusker/Views/Tab/TabTableViewCell.swift create mode 100644 Tusker/Views/Tab/TabTableViewCell.xib diff --git a/Tusker.xcodeproj/project.pbxproj b/Tusker.xcodeproj/project.pbxproj index 88658459..b0c5e1d8 100644 --- a/Tusker.xcodeproj/project.pbxproj +++ b/Tusker.xcodeproj/project.pbxproj @@ -57,6 +57,9 @@ D6109A0D214599E100432DC2 /* RequestRange.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A0C214599E100432DC2 /* RequestRange.swift */; }; D6109A0F21459B6900432DC2 /* Pagination.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A0E21459B6900432DC2 /* Pagination.swift */; }; D6109A11214607D500432DC2 /* Timeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6109A10214607D500432DC2 /* Timeline.swift */; }; + D621544821682A9D0003D87D /* TabsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D621544721682A9D0003D87D /* TabsTableViewController.swift */; }; + D621544B21682AD30003D87D /* TabTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D621544A21682AD30003D87D /* TabTableViewCell.swift */; }; + D621544D21682AD90003D87D /* TabTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D621544C21682AD90003D87D /* TabTableViewCell.xib */; }; D6333B372137838300CE884A /* AttributedString+Trim.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B362137838300CE884A /* AttributedString+Trim.swift */; }; D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B762138D94E00CE884A /* ComposeMediaView.swift */; }; D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */; }; @@ -103,6 +106,7 @@ D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A812157E8FA00721E32 /* XCBSession.swift */; }; D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.swift */; }; D67E0513216438A7000E0927 /* AppearanceTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67E0512216438A7000E0927 /* AppearanceTableViewController.swift */; }; + D67E051521643C77000E0927 /* Tab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67E051421643C77000E0927 /* Tab.swift */; }; D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */; }; D6C693CA2161253F007D6A6D /* SilentActionPermissionsTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693C92161253F007D6A6D /* SilentActionPermissionsTableViewController.swift */; }; @@ -241,6 +245,9 @@ D6109A0C214599E100432DC2 /* RequestRange.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestRange.swift; sourceTree = ""; }; D6109A0E21459B6900432DC2 /* Pagination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pagination.swift; sourceTree = ""; }; D6109A10214607D500432DC2 /* Timeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Timeline.swift; sourceTree = ""; }; + D621544721682A9D0003D87D /* TabsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsTableViewController.swift; sourceTree = ""; }; + D621544A21682AD30003D87D /* TabTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabTableViewCell.swift; sourceTree = ""; }; + D621544C21682AD90003D87D /* TabTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TabTableViewCell.xib; sourceTree = ""; }; D6333B362137838300CE884A /* AttributedString+Trim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttributedString+Trim.swift"; sourceTree = ""; }; D6333B762138D94E00CE884A /* ComposeMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeMediaView.swift; sourceTree = ""; }; D6333B782139AEFD00CE884A /* Date+TimeAgo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+TimeAgo.swift"; sourceTree = ""; }; @@ -286,6 +293,7 @@ D6757A812157E8FA00721E32 /* XCBSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBSession.swift; sourceTree = ""; }; D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.swift; sourceTree = ""; }; D67E0512216438A7000E0927 /* AppearanceTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearanceTableViewController.swift; sourceTree = ""; }; + D67E051421643C77000E0927 /* Tab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tab.swift; sourceTree = ""; }; D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSoup.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = ""; }; D6C693C92161253F007D6A6D /* SilentActionPermissionsTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SilentActionPermissionsTableViewController.swift; sourceTree = ""; }; @@ -464,6 +472,15 @@ path = Model; sourceTree = ""; }; + D621544921682AC60003D87D /* Tab */ = { + isa = PBXGroup; + children = ( + D621544C21682AD90003D87D /* TabTableViewCell.xib */, + D621544A21682AD30003D87D /* TabTableViewCell.swift */, + ); + path = Tab; + sourceTree = ""; + }; D641C780213DD7C4004B4513 /* Screens */ = { isa = PBXGroup; children = ( @@ -558,8 +575,9 @@ isa = PBXGroup; children = ( D663626521360DD700C9CBA2 /* Preferences.storyboard */, - D67E0512216438A7000E0927 /* AppearanceTableViewController.swift */, D663626721360E2C00C9CBA2 /* PreferencesTableViewController.swift */, + D67E0512216438A7000E0927 /* AppearanceTableViewController.swift */, + D621544721682A9D0003D87D /* TabsTableViewController.swift */, D641C78E213DF2AA004B4513 /* VisibilityTableViewController.swift */, D6C693C92161253F007D6A6D /* SilentActionPermissionsTableViewController.swift */, ); @@ -620,6 +638,7 @@ D663626121360B1900C9CBA2 /* Preferences.swift */, D663626321360D2300C9CBA2 /* AvatarStyle.swift */, D66362692136163000C9CBA2 /* PreferencesAdaptive.swift */, + D67E051421643C77000E0927 /* Tab.swift */, ); path = Preferences; sourceTree = ""; @@ -653,6 +672,7 @@ D6BED1722126661300F02DA0 /* Views */ = { isa = PBXGroup; children = ( + D621544921682AC60003D87D /* Tab */, 04496BD621625361001F1B23 /* ContentLabel.swift */, D6C693F82162E4DB007D6A6D /* StatusContentLabel.swift */, D6C94D882139E6EC00CB5196 /* AttachmentView.swift */, @@ -995,6 +1015,7 @@ D641C771213CA9EC004B4513 /* Notifications.storyboard in Resources */, D663626621360DD700C9CBA2 /* Preferences.storyboard in Resources */, D667E5E12134937B0057A976 /* StatusTableViewCell.xib in Resources */, + D621544D21682AD90003D87D /* TabTableViewCell.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1085,9 +1106,11 @@ D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */, D66362712136338600C9CBA2 /* ComposeViewController.swift in Sources */, D6028B9B2150811100F223B9 /* MastodonCache.swift in Sources */, + D67E051521643C77000E0927 /* Tab.swift in Sources */, D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */, D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */, D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */, + D621544B21682AD30003D87D /* TabTableViewCell.swift in Sources */, 04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */, D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */, 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */, @@ -1102,6 +1125,7 @@ D6434EB3215B1856001A919A /* XCBRequest.swift in Sources */, D663626221360B1900C9CBA2 /* Preferences.swift in Sources */, D6333B792139AEFD00CE884A /* Date+TimeAgo.swift in Sources */, + D621544821682A9D0003D87D /* TabsTableViewController.swift in Sources */, D641C78F213DF2AA004B4513 /* VisibilityTableViewController.swift in Sources */, D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */, 04496BD721625361001F1B23 /* ContentLabel.swift in Sources */, diff --git a/Tusker/Preferences/Preferences.swift b/Tusker/Preferences/Preferences.swift index 207e3d01..59e4f3fc 100644 --- a/Tusker/Preferences/Preferences.swift +++ b/Tusker/Preferences/Preferences.swift @@ -31,16 +31,29 @@ class Preferences: Codable { return Preferences() } + private init() {} + + // MARK: - Appearance var showRepliesInProfiles = false - var avatarStyle = AvatarStyle.roundRect - var hideCustomEmojiInUsernames = false + var tabs: [Tab: Int] = [.home: 0, .notifications: 1, .local: -1, .federated: 2, .myProfile: 3, .preferences: 4] var defaultPostVisibility = Status.Visibility.public + // MARK: - Advanced var silentActions: [String: Permission] = [:] + // MARK: - Utility Methods + func tabIndex(_ tab: Tab) -> Int { + if let index = tabs[tab] { + return index + } else { + tabs[tab] = -1 + return -1 + } + } + } extension Preferences { diff --git a/Tusker/Preferences/Tab.swift b/Tusker/Preferences/Tab.swift new file mode 100644 index 00000000..34ede192 --- /dev/null +++ b/Tusker/Preferences/Tab.swift @@ -0,0 +1,35 @@ +// +// Tab.swift +// Tusker +// +// Created by Shadowfacts on 10/2/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import Foundation + +enum Tab: String, Codable, CaseIterable { + case home + case federated + case local + case myProfile + case notifications + case preferences + + var humanName: String { + switch self { + case .home: + return "Home" + case .federated: + return "Federated" + case .local: + return "Local" + case .myProfile: + return "My Profile" + case .notifications: + return "Notifications" + case .preferences: + return "Preferences" + } + } +} diff --git a/Tusker/Screens/Main/MainTabBarViewController.swift b/Tusker/Screens/Main/MainTabBarViewController.swift index 8c12b4c3..898f214f 100644 --- a/Tusker/Screens/Main/MainTabBarViewController.swift +++ b/Tusker/Screens/Main/MainTabBarViewController.swift @@ -13,35 +13,49 @@ class MainTabBarViewController: UITabBarController { override func viewDidLoad() { super.viewDidLoad() - let home = TimelineTableViewController.create(for: .home) - - let federated = TimelineTableViewController.create(for: .public(local: false)) - - let local = TimelineTableViewController.create(for: .public(local: true)) - - let ownProfile = ProfileTableViewController.createForPending() - ownProfile.title = "My Profile" - - let notifications = NotificationsTableViewController.create() - - let preferences = PreferencesTableViewController.create() - - viewControllers = [ - navigationController(for: home), - navigationController(for: federated), - navigationController(for: local), - navigationController(for: ownProfile), - notifications, - preferences - ] - - MastodonController.getOwnAccount { (account) in - ownProfile.accountID = account.id + updateTabs(animated: false) + } + + func updateTabs(animated: Bool) { + let currentTabs = Preferences.shared.tabs.filter { $1 >= 0 }.sorted { $1.1 > $0.1 }.map { $0.key } + let viewControllers: [UIViewController] = currentTabs.map { (tab) in + if tab == .preferences, let preferences = selectedViewController { + return preferences + } else { + return embedInNavigationController(createVC(for: tab)) + } + } + setViewControllers(viewControllers, animated: animated) + } + + func createVC(for tab: Tab) -> UIViewController { + switch tab { + case .home: + return TimelineTableViewController.create(for: .home) + case .federated: + return TimelineTableViewController.create(for: .public(local: false)) + case .local: + return TimelineTableViewController.create(for: .public(local: true)) + case .myProfile: + let myProfile = ProfileTableViewController.createForPending() + myProfile.title = "My Profile" + MastodonController.getOwnAccount { (account) in + myProfile.accountID = account.id + } + return myProfile + case .notifications: + return NotificationsTableViewController.create() + case .preferences: + return PreferencesTableViewController.create() } } - func navigationController(for vc: UIViewController) -> UINavigationController { - return UINavigationController(rootViewController: vc) + func embedInNavigationController(_ vc: UIViewController) -> UINavigationController { + if let vc = vc as? UINavigationController { + return vc + } else { + return UINavigationController(rootViewController: vc) + } } /* diff --git a/Tusker/Screens/Preferences/Preferences.storyboard b/Tusker/Screens/Preferences/Preferences.storyboard index a2e9ae7d..74510892 100644 --- a/Tusker/Screens/Preferences/Preferences.storyboard +++ b/Tusker/Screens/Preferences/Preferences.storyboard @@ -1,10 +1,10 @@ - + - + @@ -44,22 +44,22 @@ - + - + - + - - - - - + + + + - + - + - + - + - - - + + - + @@ -114,7 +114,7 @@ - + @@ -130,10 +130,10 @@ - + - + @@ -162,7 +162,7 @@ - + @@ -190,7 +190,7 @@ - + @@ -219,6 +219,33 @@ + + + + + + + + + + + + + + + + + + + + + + @@ -234,7 +261,26 @@ - + + + + + + + + + + + + + + + + + + + + @@ -273,7 +319,7 @@ - + @@ -321,7 +367,7 @@ - + diff --git a/Tusker/Screens/Preferences/TabsTableViewController.swift b/Tusker/Screens/Preferences/TabsTableViewController.swift new file mode 100644 index 00000000..e9c03620 --- /dev/null +++ b/Tusker/Screens/Preferences/TabsTableViewController.swift @@ -0,0 +1,92 @@ +// +// TabsTableViewController.swift +// Tusker +// +// Created by Shadowfacts on 10/5/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit + +class TabsTableViewController: UITableViewController { + + var tabs: [(Tab, Bool)] = [] + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.register(UINib(nibName: "TabTableViewCell", bundle: nil), forCellReuseIdentifier: "tabCell") + + tabs = Preferences.shared.tabs.filter { $0.value >= 0 }.sorted(by: { $0.value < $1.value }).map { ($0.key, true) } + tabs.append(contentsOf: Preferences.shared.tabs.filter { $0.value == -1 }.map { ($0.key, false) }) + + tableView.setEditing(true, animated: false) + } + + func updatePreferences() { + var dict = [Tab: Int]() + var maxIndex = 0 + for (tab, enabled) in tabs { + if enabled { + dict[tab] = maxIndex + maxIndex += 1 + } else { + dict[tab] = -1 + } + } + Preferences.shared.tabs = dict + + guard let tabBarController = tabBarController as? MainTabBarViewController else { fatalError("Tab bar controller should be of type MainTabBarViewController") } + tabBarController.updateTabs(animated: true) + } + + // MARK: - Table view data source + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return tabs.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard let cell = tableView.dequeueReusableCell(withIdentifier: "tabCell", for: indexPath) as? TabTableViewCell else { fatalError() } + + let (tab, _) = tabs[indexPath.row] + cell.updateUI(for: tab) + cell.delegate = self + + return cell + } + + override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { + return .none + } + + override func tableView(_ tableView: UITableView, moveRowAt sourceIndexPath: IndexPath, to destinationIndexPath: IndexPath) { + let tab = tabs.remove(at: sourceIndexPath.row) + tabs.insert(tab, at: destinationIndexPath.row) + updatePreferences() + } + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ + +} + +extension TabsTableViewController: TabTableViewCellDelegate { + func setEnabled(tab: Tab, enabled: Bool) { + let index = tabs.firstIndex(where: { $0.0 == tab })! + tabs[index] = (tab, enabled) + updatePreferences() + } +} + diff --git a/Tusker/Views/Tab/TabTableViewCell.swift b/Tusker/Views/Tab/TabTableViewCell.swift new file mode 100644 index 00000000..23ccde0e --- /dev/null +++ b/Tusker/Views/Tab/TabTableViewCell.swift @@ -0,0 +1,47 @@ +// +// TabTableViewCell.swift +// Tusker +// +// Created by Shadowfacts on 10/5/18. +// Copyright © 2018 Shadowfacts. All rights reserved. +// + +import UIKit + +protocol TabTableViewCellDelegate { + func setEnabled(tab: Tab, enabled: Bool) +} + +class TabTableViewCell: UITableViewCell { + + var delegate: TabTableViewCellDelegate? + + @IBOutlet weak var nameLabel: UILabel! + @IBOutlet weak var enabledSwitch: UISwitch! + + var tab: Tab! + + override func awakeFromNib() { + super.awakeFromNib() + + // compensate for inset created by table view editing mode even when no delete control shown + nameLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: -20).isActive = true + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + } + + func updateUI(for tab: Tab) { + self.tab = tab + + enabledSwitch.isEnabled = tab != .preferences + enabledSwitch.setOn(Preferences.shared.tabIndex(tab) >= 0, animated: false) + nameLabel.text = tab.humanName + } + + @IBAction func enabledSwitchChanged(_ sender: Any) { + delegate?.setEnabled(tab: tab, enabled: enabledSwitch.isOn) + } + +} diff --git a/Tusker/Views/Tab/TabTableViewCell.xib b/Tusker/Views/Tab/TabTableViewCell.xib new file mode 100644 index 00000000..d6c353d4 --- /dev/null +++ b/Tusker/Views/Tab/TabTableViewCell.xib @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +