Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions Sources/FuturedArchitecture/Architecture/ModalCoverModifier.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import SwiftUI

/// Adds `.sheet` and (on non-macOS platforms) `.fullScreenCover` presentation
/// driven by a ``Coordinator``'s ``Coordinator/modalCover`` state.
///
/// Use this modifier in any flow container view (e.g. ``NavigationStackFlow``,
/// ``TabViewFlow``, ``TabContentFlow``) to keep modal presentation behaviour consistent.
struct ModalCoverModifier<C: Coordinator>: ViewModifier {
var coordinator: C

private var sheetBinding: Binding<C.Destination?> {
.init {
coordinator.modalCover?.style == .sheet ? coordinator.modalCover?.destination : nil
} set: { destination in
coordinator.modalCover = destination.map { .init(destination: $0, style: .sheet) }
}
}

#if os(macOS)
func body(content: Content) -> some View {
content
.sheet(item: sheetBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:))
}
#else
private var fullscreenCoverBinding: Binding<C.Destination?> {
.init {
coordinator.modalCover?.style == .fullscreenCover ? coordinator.modalCover?.destination : nil
} set: { destination in
coordinator.modalCover = destination.map { .init(destination: $0, style: .fullscreenCover) }
}
}

func body(content: Content) -> some View {
content
.sheet(item: sheetBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:))
.fullScreenCover(item: fullscreenCoverBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:))
}
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,29 +29,8 @@ public struct NavigationStackFlow<Coordinator: NavigationStackCoordinator, Conte
content().navigationDestination(for: Coordinator.Destination.self, destination: coordinator.scene(for:))
}
.modifier(OptionalPresentationDetentsModifier(detents: navigationDetents))
.sheet(item: sheetBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:))
#if !os(macOS)
.fullScreenCover(item: fullscreenCoverBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:))
#endif
.modifier(ModalCoverModifier(coordinator: coordinator))
}

private var sheetBinding: Binding<Coordinator.Destination?> {
.init {
coordinator.modalCover?.style == .sheet ? coordinator.modalCover?.destination : nil
} set: { destination in
coordinator.modalCover = destination.map { .init(destination: $0, style: .sheet) }
}
}

#if !os(macOS)
private var fullscreenCoverBinding: Binding<Coordinator.Destination?> {
.init {
coordinator.modalCover?.style == .fullscreenCover ? coordinator.modalCover?.destination : nil
} set: { destination in
coordinator.modalCover = destination.map { .init(destination: $0, style: .fullscreenCover) }
}
}
#endif
}

/// A view modifier that conditionally applies presentation detents only when they are non-nil.
Expand Down
64 changes: 40 additions & 24 deletions Sources/FuturedArchitecture/Architecture/TabViewFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import SwiftUI

/// The `TabViewFlow` encapsulates the ``SwiftUI.TabView`` and binds it to the
/// variables and callbacks of the ``TabCoordinator`` which it retains as a ``SwiftUI.State``.
///
/// Uses the legacy `.tabItem` + `.tag` pattern. For iOS 18+ projects that need
/// ``SwiftUI.Tab`` features (e.g. `Tab(role: .search)`), use ``TabContentFlow`` instead.
/// - Experiment: This API is in preview and subject to change.
@available(iOS, deprecated: 18.0, message: "Use TabContentFlow for the iOS 18+ Tab API.")
@available(macOS, deprecated: 15.0, message: "Use TabContentFlow for the macOS 15+ Tab API.")
@available(tvOS, deprecated: 18.0, message: "Use TabContentFlow for the tvOS 18+ Tab API.")
@available(watchOS, deprecated: 11.0, message: "Use TabContentFlow for the watchOS 11+ Tab API.")
public struct TabViewFlow<Coordinator: TabCoordinator, Content: View>: View {
@State private var coordinator: Coordinator
@ViewBuilder private let content: () -> Content
Expand All @@ -16,38 +23,47 @@ public struct TabViewFlow<Coordinator: TabCoordinator, Content: View>: View {
self.content = content
}

#if os(macOS)
public var body: some View {
TabView(selection: $coordinator.selectedTab) {
content()
}
.sheet(item: sheetBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:))
.modifier(ModalCoverModifier(coordinator: coordinator))
}
#else
public var body: some View {
TabView(selection: $coordinator.selectedTab) {
content()
}
.sheet(item: sheetBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:))
.fullScreenCover(item: fullscreenCoverBinding, onDismiss: coordinator.onModalDismiss, content: coordinator.scene(for:))
}
#endif
}

private var sheetBinding: Binding<Coordinator.Destination?> {
.init {
coordinator.modalCover?.style == .sheet ? coordinator.modalCover?.destination : nil
} set: { destination in
coordinator.modalCover = destination.map { .init(destination: $0, style: .sheet) }
}
// MARK: - TabContentFlow (iOS 18+)

/// A variant of ``TabViewFlow`` that accepts ``SwiftUI.TabContent`` instead of `View`,
/// enabling the iOS 18+ ``SwiftUI.Tab`` API with features like `Tab(role: .search)`.
///
/// Use this instead of ``TabViewFlow`` when you need `Tab(role:)`, customizable tab
/// placement, or other iOS 18+ tab features.
///
/// Usage:
/// ```swift
/// TabContentFlow(coordinator: instance) {
/// Tab("Home", systemImage: "house", value: Tab.home) { ... }
/// Tab(value: Tab.search, role: .search) { ... }
/// }
/// ```
/// - Experiment: This API is in preview and subject to change.
@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, *)
public struct TabContentFlow<Coordinator: TabCoordinator, Content: TabContent<Coordinator.Tab>>: View {
Comment on lines +50 to +51

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Zaroven by som pridal deprecation flag na tú pôvodnú štruktúru TabViewFlow

@State private var coordinator: Coordinator
@TabContentBuilder<Coordinator.Tab> private let content: () -> Content

/// - Parameters:
/// - coordinator: The instance of the coordinator used as the model and retained as ``SwiftUI.State``
/// - content: The definition of tabs using the ``SwiftUI.Tab`` API.
public init(coordinator: Coordinator, @TabContentBuilder<Coordinator.Tab> content: @MainActor @escaping () -> Content) {
self._coordinator = State(wrappedValue: coordinator)
self.content = content
}

#if !os(macOS)
private var fullscreenCoverBinding: Binding<Coordinator.Destination?> {
.init {
coordinator.modalCover?.style == .fullscreenCover ? coordinator.modalCover?.destination : nil
} set: { destination in
coordinator.modalCover = destination.map { .init(destination: $0, style: .fullscreenCover) }
public var body: some View {
TabView(selection: $coordinator.selectedTab) {
content()
}
.modifier(ModalCoverModifier(coordinator: coordinator))
}
#endif
}