Skip to content

Commit

Permalink
Fix "Official Community" link, Improve navigation routing pattern (#656)
Browse files Browse the repository at this point in the history
  • Loading branch information
boscojwho committed Nov 23, 2023
1 parent 5346be7 commit 09187d6
Show file tree
Hide file tree
Showing 29 changed files with 359 additions and 319 deletions.
79 changes: 34 additions & 45 deletions Mlem.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

17 changes: 2 additions & 15 deletions Mlem/Extensions/Navigation getter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,12 @@ extension EnvironmentValues {
// MARK: - Mlem NavigationRoute

private struct NavigationPathWithRoutes: EnvironmentKey {
static let defaultValue: Binding<[NavigationRoute]> = .constant([])
static let defaultValue: Binding<[AppRoute]> = .constant([])
}

extension EnvironmentValues {
var navigationPathWithRoutes: Binding<[NavigationRoute]> {
var navigationPathWithRoutes: Binding<[AppRoute]> {
get { self[NavigationPathWithRoutes.self] }
set { self[NavigationPathWithRoutes.self] = newValue }
}
}

// MARK: - Mlem SettingsRoute

struct NavigationPathWithSettingsRoutes: EnvironmentKey {
static let defaultValue: Binding<[SettingsRoute]> = .constant([])
}

extension EnvironmentValues {
var settingsRoutesNavigationPath: Binding<[SettingsRoute]> {
get { self[NavigationPathWithSettingsRoutes.self] }
set { self[NavigationPathWithSettingsRoutes.self] = newValue }
}
}
104 changes: 101 additions & 3 deletions Mlem/Extensions/View - Handle Lemmy Links.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import Foundation
import SwiftUI

struct HandleLemmyLinksDisplay: ViewModifier {
@Environment(\.navigationPath) private var navigationPath
@EnvironmentObject private var layoutWidgetTracker: LayoutWidgetTracker
@EnvironmentObject var appState: AppState
@EnvironmentObject var filtersTracker: FiltersTracker

Expand All @@ -18,9 +20,10 @@ struct HandleLemmyLinksDisplay: ViewModifier {
@AppStorage("upvoteOnSave") var upvoteOnSave = false

// swiftlint:disable function_body_length
// swiftlint:disable:next cyclomatic_complexity
func body(content: Content) -> some View {
content
.navigationDestination(for: NavigationRoute.self) { route in
.navigationDestination(for: AppRoute.self) { route in
switch route {
case .apiCommunity(let community):
FeedView(community: community, feedType: .all, sortType: defaultPostSorting)
Expand Down Expand Up @@ -71,13 +74,108 @@ struct HandleLemmyLinksDisplay: ViewModifier {
case .userModeratorLink(let user):
UserModeratorView(userDetails: user.user, moderatedCommunities: user.moderatedCommunities)
.environmentObject(appState)
case .settings(let page):
settingsDestination(for: page)
case .aboutSettings(let page):
aboutSettingsDestination(for: page)
case .appearanceSettings(let page):
appearanceSettingsDestination(for: page)
case .commentSettings(let page):
commentSettingsDestination(for: page)
case .postSettings(let page):
postSettingsDestination(for: page)
case .licenseSettings(let page):
licensesSettingsDestination(for: page)
}
}
}
// swiftlint:enable function_body_length

@ViewBuilder
private func settingsDestination(for page: SettingsPage) -> some View {
switch page {
case .accounts:
AccountsPage()
case .general:
GeneralSettingsView()
case .accessibility:
AccessibilitySettingsView()
case .appearance:
AppearanceSettingsView()
case .contentFilters:
FiltersSettingsView()
case .about:
AboutView()
case .advanced:
AdvancedSettingsView()
}
}

@ViewBuilder
private func aboutSettingsDestination(for page: AboutSettingsPage) -> some View {
switch page {
case .contributors:
ContributorsView()
case let .document(doc):
DocumentView(text: doc.body)
case .licenses:
LicensesView()
}
}

@ViewBuilder
private func appearanceSettingsDestination(for page: AppearanceSettingsPage) -> some View {
switch page {
case .theme:
ThemeSettingsView()
case .appIcon:
IconSettingsView()
case .posts:
PostSettingsView()
case .comments:
CommentSettingsView()
case .communities:
CommunitySettingsView()
case .users:
UserSettingsView()
case .tabBar:
TabBarSettingsView()
}
}

@ViewBuilder
private func commentSettingsDestination(for page: CommentSettingsPage) -> some View {
switch page {
case .layoutWidget:
LayoutWidgetEditView(widgets: layoutWidgetTracker.groups.comment, onSave: { widgets in
layoutWidgetTracker.groups.comment = widgets
layoutWidgetTracker.saveLayoutWidgets()
})
}
}

@ViewBuilder
private func postSettingsDestination(for page: PostSettingsPage) -> some View {
switch page {
case .customizeWidgets:
/// We really should be passing in the layout widget through the route enum value, but that would involve making layout widget tracker hashable and codable.
LayoutWidgetEditView(widgets: layoutWidgetTracker.groups.post, onSave: { widgets in
layoutWidgetTracker.groups.post = widgets
layoutWidgetTracker.saveLayoutWidgets()
})
}
}

@ViewBuilder
private func licensesSettingsDestination(for page: LicensesSettingsPage) -> some View {
switch page {
case let .licenseDocument(doc):
DocumentView(text: doc.body)
}
}
}

struct HandleLemmyLinkResolution<Path: AnyNavigationPath>: ViewModifier {
struct HandleLemmyLinkResolution<Path: AnyNavigablePath>: ViewModifier {
@Dependency(\.apiClient) var apiClient
@Dependency(\.errorHandler) var errorHandler
@Dependency(\.notifier) var notifier
Expand Down Expand Up @@ -181,7 +279,7 @@ extension View {
modifier(HandleLemmyLinksDisplay())
}

func handleLemmyLinkResolution<P: AnyNavigationPath>(navigationPath: Binding<P>) -> some View {
func handleLemmyLinkResolution<P: AnyNavigablePath>(navigationPath: Binding<P>) -> some View {
modifier(HandleLemmyLinkResolution(navigationPath: navigationPath))
}
}
31 changes: 31 additions & 0 deletions Mlem/Navigation/AnyNavigablePath.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// AnyNavigablePath.swift
// Mlem
//
// Created by Bosco Ho on 2023-09-08.
//

import Foundation
import SwiftUI

protocol AnyNavigablePath {

associatedtype Route: Routable

/// Implementation should make a route that makes sense for the passed-in data value and can be appended to the navigation path.
static func makeRoute<V>(_ value: V) throws -> Route where V: Hashable

/// The number of elements in this path.
var count: Int { get }

/// A Boolean that indicates whether this path is empty.
var isEmpty: Bool { get }

/// Appends a new value to the end of this path.
mutating func append<V>(_ value: V) where V: Routable

// swiftlint:disable identifier_name
/// Removes values from the end of this path.
mutating func removeLast(_ k: Int)
// swiftlint:enable identifier_name
}
45 changes: 32 additions & 13 deletions Mlem/Navigation/AnyNavigationPath.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,45 @@
//

import Foundation
import SwiftUI

protocol AnyNavigationPath {
/// For when the system `NavigationPath` doesn't meet your needs.
///
/// Technical Note:
/// - [2023.09] Initially, enum-based navigation routes were added during the development of tab-bar navigation. When using the system `NavigationPath`, the UI would exhibit a bug where views would randomly push onto view without any animations, after which the navigation path became corrupt, making programmatic navigation unreliable. Using enum-based navigation routes with custom navigation paths resulted in this issue disappearing on both iOS 16/17.
final class AnyNavigationPath<RouteValue: Routable>: ObservableObject {

associatedtype Route: Routable
/// - Avoid directly manipulating this value, if alternate methods are provided.
@Published var path: [RouteValue] = []

/// Implementation should make a route that makes sense for the passed-in data value and can be appended to the navigation path.
static func makeRoute<V>(_ value: V) throws -> Route where V: Hashable
}

extension AnyNavigationPath: AnyNavigablePath {

typealias Route = RouteValue

static func makeRoute<V>(_ value: V) throws -> Route where V: Hashable {
try RouteValue.makeRoute(value)
}

/// The number of elements in this path.
var count: Int { get }
var count: Int {
path.count
}

/// A Boolean that indicates whether this path is empty.
var isEmpty: Bool { get }
var isEmpty: Bool {
path.isEmpty
}

/// Appends a new value to the end of this path.
mutating func append<V>(_ value: V) where V: Routable
func append<V>(_ value: V) where V: Routable {
guard let route = value as? Route else {
assert(value is Route)
return
}
path.append(route)
}

// swiftlint:disable identifier_name
/// Removes values from the end of this path.
mutating func removeLast(_ k: Int)
func removeLast(_ k: Int = 1) {
path.removeLast(k)
}
// swiftlint:enable identifier_name
}
20 changes: 20 additions & 0 deletions Mlem/Navigation/Destination Values/DestinationValue.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// DestinationValue.swift
// Mlem
//
// Created by Bosco Ho on 2023-09-26.
//

import Foundation

/// Essentially a cheap "view-model", wrap `DestinationValue` inside a `Routable` value, then use that value as the data to define navigation destinations.
///
/// Conforming types can be used to drive value-based navigation for destinations that are defined semantically or are not (yet) mapped to a particular data-type or view model.
///
/// For example:
/// - Many `Settings` views are presented based on their purpose and not the data they present.
/// - In this scenario, we can define a set of semantically named enum cases (i.e. `.general` or `.about`), and treat these enum cases as values that drive navigation.
/// - See `AppRoute` settings routes for an example implementation.
///
/// - Warning: Avoid directly adding `DestinationValue` to a navigation path or using them as data to define `navigationDestination(...)`.
protocol DestinationValue: Hashable {}
47 changes: 47 additions & 0 deletions Mlem/Navigation/Destination Values/SettingsValues.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
//
// SettingsRoutes.swift
// Mlem
//
// Created by Bosco Ho on 2023-08-18.
//

import Foundation

enum SettingsPage: DestinationValue {
case accounts
case general
case accessibility
case appearance
case contentFilters
case about
case advanced
}

enum AboutSettingsPage: DestinationValue {
case contributors
/// e.g. `Privacy Policy` or `EULA`.
case document(Document)
case licenses
}

enum AppearanceSettingsPage: DestinationValue {
case theme
case appIcon
case posts
case comments
case communities
case users
case tabBar
}

enum CommentSettingsPage: DestinationValue {
case layoutWidget
}

enum PostSettingsPage: DestinationValue {
case customizeWidgets
}

enum LicensesSettingsPage: DestinationValue {
case licenseDocument(Document)
}
14 changes: 2 additions & 12 deletions Mlem/Navigation/DismissAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ struct NavigationDismissHoisting: ViewModifier {
@EnvironmentObject private var navigation: Navigation

@Environment(\.navigationPathWithRoutes) private var routesNavigationPath
@Environment(\.settingsRoutesNavigationPath) private var settingsNavigationPath

@Environment(\.tabSelectionHashValue) private var selectedTabHashValue

Expand All @@ -62,11 +61,7 @@ struct NavigationDismissHoisting: ViewModifier {
assertionFailure()
return []
}
if selectedTabHashValue == TabSelection.settings.hashValue {
return settingsNavigationPath.wrappedValue
} else {
return routesNavigationPath.wrappedValue
}
return routesNavigationPath.wrappedValue
}

/// - Note: Unfortunately, we can't access the dismiss action via View.environment...doing so causes SwiftUI to enter into infinite loop. [2023.09]
Expand Down Expand Up @@ -123,7 +118,6 @@ struct PerformTabBarNavigation: ViewModifier {
@Dependency(\.hapticManager) private var hapticManager

@Environment(\.navigationPathWithRoutes) private var routesNavigationPath
@Environment(\.settingsRoutesNavigationPath) private var settingsNavigationPath

@Environment(\.tabSelectionHashValue) private var selectedTabHashValue
@Environment(\.tabNavigationSelectionHashValue) private var selectedNavigationTabHashValue
Expand All @@ -136,11 +130,7 @@ struct PerformTabBarNavigation: ViewModifier {
assertionFailure()
return []
}
if selectedTabHashValue == TabSelection.settings.hashValue {
return settingsNavigationPath.wrappedValue
} else {
return routesNavigationPath.wrappedValue
}
return routesNavigationPath.wrappedValue
}

let tab: TabSelection
Expand Down
2 changes: 1 addition & 1 deletion Mlem/Navigation/NavigationLink+Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import SwiftUI
extension NavigationLink where Destination == Never {

/// Convenience initializer.
init(_ route: NavigationRoute, @ViewBuilder label: () -> Label) {
init(_ route: AppRoute, @ViewBuilder label: () -> Label) {
self = .init(value: route, label: label)
}
}
Loading

0 comments on commit 09187d6

Please sign in to comment.