Skip to content

Commit

Permalink
Merge branch 'dev' into sjmarf/modlog-filters
Browse files Browse the repository at this point in the history
  • Loading branch information
Sjmarf authored Jan 12, 2025
2 parents 09b16a1 + ac18da8 commit 796aa26
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 72 deletions.
5 changes: 4 additions & 1 deletion Mlem/App/Views/Root/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ struct ContentView: View {
try await (appState.firstSession as? UserSession)?.unreadCount?.refresh()
}
}
.navigationSheetModifiers(nextLayer: navigationModel.layers.first, model: navigationModel)
.navigationSheetModifiers(
nextLayer: navigationModel.layers.first,
contentPickerTracker: navigationModel.contentPickerTracker
)
.tint(palette.accent)
.environment(palette)
.environment(tabReselectTracker)
Expand Down
10 changes: 6 additions & 4 deletions Mlem/App/Views/Shared/Navigation/NavigationLayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import MlemMiddleware
import SwiftUI

@Observable
class NavigationLayer {
class NavigationLayer: Identifiable {
struct ShareInfo {
let url: URL
let activities: [ShareActivity]
Expand All @@ -31,6 +31,8 @@ class NavigationLayer {
}
}

var id: ObjectIdentifier { .init(self) }

weak var model: NavigationModel?
var index: Int

Expand Down Expand Up @@ -125,7 +127,7 @@ class NavigationLayer {

@MainActor
func showPhotosPicker(for imageUploadManager: ImageUploadManager, api: ApiClient) {
model?.photosPickerCallback = { photo in
model?.contentPickerTracker.photosPickerCallback = { photo in
Task {
do {
guard let data = try await photo.loadTransferable(type: Data.self) else {
Expand All @@ -145,8 +147,8 @@ class NavigationLayer {

@MainActor
func showFilePicker(for imageUploadManager: ImageUploadManager, api: ApiClient) {
model?.showingFilePicker = true
model?.filePickerCallback = { url in
model?.contentPickerTracker.showingFilePicker = true
model?.contentPickerTracker.filePickerCallback = { url in
Task {
do {
guard url.startAccessingSecurityScopedResource() else {
Expand Down
19 changes: 12 additions & 7 deletions Mlem/App/Views/Shared/Navigation/NavigationModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,19 @@ class NavigationModel {

private(set) var layers: [NavigationLayer] = .init()

var photosPickerCallback: ((PhotosPickerItem) -> Void)?
@Observable
class ContentPickerTracker {
var photosPickerCallback: ((PhotosPickerItem) -> Void)?

// This needs two values unlike `photosPickerCallback` because
// `fileImporter` sets `isPresented` to `false` before calling
// `onCompletion`, which makes it impossible to call the callback
// before setting it to `nil`.
var showingFilePicker: Bool = false
var filePickerCallback: ((URL) -> Void)?
}

// This needs two values unlike `photosPickerCallback` because
// `fileImporter` sets `isPresented` to `false` before calling
// `onCompletion`, which makes it impossible to call the callback
// before setting it to `nil`.
var showingFilePicker: Bool = false
var filePickerCallback: ((URL) -> Void)?
var contentPickerTracker: ContentPickerTracker = .init()

var mediaUrl: URL?

Expand Down
164 changes: 104 additions & 60 deletions Mlem/App/Views/Shared/Navigation/View+NavigationSheetModifiers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,73 +7,117 @@

import SwiftUI

extension View {
@ViewBuilder func navigationSheetModifiers(for layer: NavigationLayer) -> some View {
if let model = layer.model {
navigationSheetModifiers(
nextLayer: (layer.index < model.layers.count - 1) ? model.layers[layer.index + 1] : nil,
model: model
private struct NavigationSheetModifier: ViewModifier {
let nextLayer: NavigationLayer?
let contentPickerTracker_: () -> NavigationModel.ContentPickerTracker?

init(
nextLayer: NavigationLayer?,
// This tomfoolery exists to prevent this view being subject to NavigationModel view updates, which caused #1492
contentPickerTracker: @escaping () -> NavigationModel.ContentPickerTracker?
) {
self.nextLayer = nextLayer
self.contentPickerTracker_ = contentPickerTracker
}

// DO NOT access this in the view body; see #1492
var contentPickerTracker: NavigationModel.ContentPickerTracker? {
contentPickerTracker_()
}

func body(content: Content) -> some View {
content
.sheet(isPresented: Binding(
get: { !(nextLayer?.isFullScreenCover ?? true) },
set: {
if !$0 { closeSheet() }
}
)) {
if let nextLayer {
NavigationLayerView(layer: nextLayer, hasSheetModifiers: true)
}
}
.fullScreenCover(isPresented: Binding(
get: { nextLayer?.isFullScreenCover ?? false },
set: {
if !$0 { closeSheet() }
}
)) {
if let nextLayer {
NavigationLayerView(layer: nextLayer, hasSheetModifiers: true)
}
}
.photosPicker(
isPresented: .init(
get: { nextLayer == nil && contentPickerTracker?.photosPickerCallback != nil },
set: { contentPickerTracker?.photosPickerCallback = $0 ? contentPickerTracker?.photosPickerCallback : nil }
),
selection: .init(get: { nil }, set: { photo in
if let photo {
contentPickerTracker?.photosPickerCallback?(photo)
contentPickerTracker?.photosPickerCallback = nil
}
}),
matching: .images
)
.fileImporter(
isPresented: .init(
get: { nextLayer == nil && (contentPickerTracker?.showingFilePicker ?? false) },
set: { contentPickerTracker?.showingFilePicker = $0 }
),
allowedContentTypes: [.image],
onCompletion: { result in
do {
try contentPickerTracker?.filePickerCallback?(result.get())
} catch {
handleError(error)
}
}
)
}

func closeSheet() {
if let nextLayer, let model = nextLayer.model {
model.closeSheets(aboveIndex: nextLayer.index)
}
}
}

private struct ComputeNextLayerModifier: ViewModifier {
let layer: NavigationLayer

// This exists to prevent the view from being subject to NavigationModel state updates, which caused #1492
@State var nextLayer: NavigationLayer?

func body(content: Content) -> some View {
Group {
content.navigationSheetModifiers(
nextLayer: nextLayer,
contentPickerTracker: layer.model?.contentPickerTracker
)
}.onChange(of: computeNextLayer()?.id, initial: true) {
nextLayer = computeNextLayer()
}
}

func computeNextLayer() -> NavigationLayer? {
if let model = layer.model {
(layer.index < model.layers.count - 1) ? model.layers[layer.index + 1] : nil
} else {
self
nil
}
}
}

extension View {
@ViewBuilder func navigationSheetModifiers(for layer: NavigationLayer) -> some View {
modifier(ComputeNextLayerModifier(layer: layer))
}

// swiftlint:disable:next function_body_length
@ViewBuilder func navigationSheetModifiers(
nextLayer: NavigationLayer?,
model: NavigationModel
contentPickerTracker: @autoclosure @escaping () -> NavigationModel.ContentPickerTracker?
) -> some View {
sheet(isPresented: Binding(
get: { !(nextLayer?.isFullScreenCover ?? true) },
set: { newValue in
if !newValue, let nextLayer {
model.closeSheets(aboveIndex: nextLayer.index)
}
}
)) {
if let nextLayer {
NavigationLayerView(layer: nextLayer, hasSheetModifiers: true)
}
}
.fullScreenCover(isPresented: Binding(
get: { nextLayer?.isFullScreenCover ?? false },
set: { newValue in
if !newValue, let nextLayer {
model.closeSheets(aboveIndex: nextLayer.index)
}
}
)) {
if let nextLayer {
NavigationLayerView(layer: nextLayer, hasSheetModifiers: true)
}
}
.photosPicker(
isPresented: .init(
get: { nextLayer == nil && model.photosPickerCallback != nil },
set: { model.photosPickerCallback = $0 ? model.photosPickerCallback : nil }
),
selection: .init(get: { nil }, set: { photo in
if let photo {
model.photosPickerCallback?(photo)
model.photosPickerCallback = nil
}
}),
matching: .images
)
.fileImporter(
isPresented: .init(
get: { nextLayer == nil && model.showingFilePicker },
set: { model.showingFilePicker = $0 }
),
allowedContentTypes: [.image],
onCompletion: { result in
do {
try model.filePickerCallback?(result.get())
} catch {
handleError(error)
}
}
)
modifier(NavigationSheetModifier(nextLayer: nextLayer, contentPickerTracker: contentPickerTracker))
}
}

0 comments on commit 796aa26

Please sign in to comment.