Skip to content

Commit

Permalink
Ensure TabIdentifiers are matched when switching between container ki…
Browse files Browse the repository at this point in the history
…nds, and add ability to manually map identifiers between modes
  • Loading branch information
mpdifran committed May 2, 2023
1 parent 4958d64 commit 8b09eec
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
FF24DF5C29BD555D009D1ECE /* MacOSTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF24DF5B29BD555D009D1ECE /* MacOSTabView.swift */; };
FF24DF5E29BD5789009D1ECE /* iPhoneTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF24DF5D29BD5789009D1ECE /* iPhoneTabView.swift */; };
FF24DF6029BD582F009D1ECE /* AppleWatchTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF24DF5F29BD582F009D1ECE /* AppleWatchTabView.swift */; };
FF3927002A01D6CF009B2657 /* FolderTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3926FF2A01D6CF009B2657 /* FolderTabView.swift */; };
FF3F099529BEA92500866251 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3F099429BEA92500866251 /* ContentView.swift */; };
FF3F099A29BFD3AC00866251 /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3F099929BFD3AC00866251 /* SidebarView.swift */; };
/* End PBXBuildFile section */
Expand All @@ -27,6 +28,7 @@
FF24DF5B29BD555D009D1ECE /* MacOSTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacOSTabView.swift; sourceTree = "<group>"; };
FF24DF5D29BD5789009D1ECE /* iPhoneTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iPhoneTabView.swift; sourceTree = "<group>"; };
FF24DF5F29BD582F009D1ECE /* AppleWatchTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleWatchTabView.swift; sourceTree = "<group>"; };
FF3926FF2A01D6CF009B2657 /* FolderTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderTabView.swift; sourceTree = "<group>"; };
FF3F099429BEA92500866251 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
FF3F099929BFD3AC00866251 /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
Expand Down Expand Up @@ -103,6 +105,7 @@
FF24DF5B29BD555D009D1ECE /* MacOSTabView.swift */,
FF24DF5D29BD5789009D1ECE /* iPhoneTabView.swift */,
FF24DF5F29BD582F009D1ECE /* AppleWatchTabView.swift */,
FF3926FF2A01D6CF009B2657 /* FolderTabView.swift */,
);
path = Tabs;
sourceTree = "<group>";
Expand Down Expand Up @@ -192,6 +195,7 @@
FF3F099529BEA92500866251 /* ContentView.swift in Sources */,
FF24DF5C29BD555D009D1ECE /* MacOSTabView.swift in Sources */,
FF24DF4629BD1DA4009D1ECE /* AdaptiveTabExampleApp.swift in Sources */,
FF3927002A01D6CF009B2657 /* FolderTabView.swift in Sources */,
FF24DF5E29BD5789009D1ECE /* iPhoneTabView.swift in Sources */,
FF3F099A29BFD3AC00866251 /* SidebarView.swift in Sources */,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,43 @@ struct AdaptiveTabExampleApp: App {

var body: some Scene {
WindowGroup {
AdaptiveTabView(
appName: "Example",
selectedTab: $selectedTab
) { (containerKind) in
MacOSTabView()
iPhoneTabView()
AppleWatchTabView()
} defaultDetail: {
ContentView(title: "Empty Details")
} sidebarExtraContent: {
SidebarView()
VStack {
Text(selectedTab.id)
AdaptiveTabView(
appName: "Example",
selectedTab: $selectedTab
) { (containerKind) in
MacOSTabView()
iPhoneTabView()
AppleWatchTabView()
if containerKind == .tabView {
FolderTabView()
}
} defaultDetail: {
ContentView(title: "Empty Details")
} sidebarExtraContent: {
SidebarView()
}
.selectedTabTransformer(transformer)
.navigationSplitViewStyle(.automatic)
}
.navigationSplitViewStyle(.automatic)
}
}

let transformer = SelectedTabTransformer { (kind, tabIdentifier) in
switch kind {
case .tabView:
let sharedTabViewIdentifiers = [
MacOSTabView.identifier,
iPhoneTabView.identifier,
AppleWatchTabView.identifier
]
if !sharedTabViewIdentifiers.contains(tabIdentifier) {
return FolderTabView.identifier
}
case .sidebarView:
break
}
return tabIdentifier
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// FolderTabView.swift
// AdaptiveTabExample
//
// Created by Mark DiFranco on 2023-05-02.
//

import SwiftUI
import AdaptiveTabView

extension FolderTabView {
static let identifier = TabIdentifier("FolderTabView")
}

struct FolderTabView: View, TitleImageProviding {
let title = "Folders"
let systemImageName = "folder"
let id = FolderTabView.identifier

private let identifiers = [1, 2, 3, 4, 5]

var body: some View {
List {
Section {
ForEach(identifiers, id: \.self) { (identifier) in
NavigationLink {
ContentView(title: "Folder \(identifier)")
} label: {
Label("Folder \(identifier)", systemImage: "folder")
}
}
}
}
}
}

struct FolderTabView_Previews: PreviewProvider {
static var previews: some View {
FolderTabView()
}
}
64 changes: 56 additions & 8 deletions Sources/AdaptiveTabView/AdaptiveTabView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,22 @@ public enum AdaptiveTabViewSplitViewKind {
public struct AdaptiveTabView<TabContent: Sequence, SidebarExtraContent: View, DefaultContentView: View, DefaultDetailView: View>: View where TabContent.Element: TabContentView {

private let appName: String
private let selectedTab: Binding<TabIdentifier>?
private let splitViewKind: AdaptiveTabViewSplitViewKind
private let tabViewBuilder: (AdaptiveTabViewContainerKind) -> TabContent
private let defaultContentBuilder: () -> DefaultContentView
private let defaultDetailBuilder: () -> DefaultDetailView
private let sidebarExtraContentBuilder: () -> SidebarExtraContent

@Binding private var selectedTab: TabIdentifier
@State private var selectedTabViewTab: TabIdentifier
@State private var selectedSidebarViewTab: TabIdentifier

@Environment(\.selectedTabTransformer) var selectedTabTransformer

/// Creates an ``AdaptiveTabView``.
/// - parameter appName: The name of the app. This appears as the navigation title of the sidebar when the kind
/// is ``AdaptiveTabViewContainerKind.sidebarKind``.
/// - parameter selectedTab: Which tab is selected when in ``AdaptiveTabViewContainerKind.tabView``.
/// is ``AdaptiveTabViewContainerKind.sidebarView``.
/// - parameter selectedTab: The identifier of the selected tab in the currently dsisplayed mode.
/// - parameter splitViewKind: The type of split view to use.
/// - parameter tabViews: A view builder to provide the views for the tabs. In ``AdaptiveTabViewContainerKind.tabView``, they appear as
/// tabs within a ``NavigationView``. In ``AdaptiveTabViewContainerKind.sidebarView``, they appear at the top of the sidebar. You can
Expand All @@ -58,20 +63,23 @@ public struct AdaptiveTabView<TabContent: Sequence, SidebarExtraContent: View, D
/// is ``AdaptiveTabViewContainerKind.sidebarView``.
public init(
appName: String,
selectedTab: Binding<TabIdentifier>? = nil,
selectedTab: Binding<TabIdentifier>,
splitViewKind: AdaptiveTabViewSplitViewKind = .threeColumn,
@SequenceBuilder tabViews: @escaping (AdaptiveTabViewContainerKind) -> TabContent,
@ViewBuilder defaultContent: @escaping () -> DefaultContentView = { EmptyView() },
@ViewBuilder defaultDetail: @escaping () -> DefaultDetailView,
@ViewBuilder sidebarExtraContent: @escaping () -> SidebarExtraContent = { EmptyView() }
) {
self.appName = appName
self.selectedTab = selectedTab
self._selectedTab = selectedTab
self.splitViewKind = splitViewKind
self.tabViewBuilder = tabViews
self.defaultContentBuilder = defaultContent
self.defaultDetailBuilder = defaultDetail
self.sidebarExtraContentBuilder = sidebarExtraContent

self._selectedTabViewTab = State(initialValue: selectedTab.wrappedValue)
self._selectedSidebarViewTab = State(initialValue: selectedTab.wrappedValue)
}

@Environment(\.horizontalSizeClass) private var horizontalSizeClass
Expand All @@ -81,13 +89,13 @@ public struct AdaptiveTabView<TabContent: Sequence, SidebarExtraContent: View, D
switch horizontalSizeClass {
case .compact:
TabLayoutView(
selectedTab: selectedTab,
selectedTab: $selectedTabViewTab,
tabViewBuilder
)
default:
SidebarLayoutView(
appName,
selectedTab: selectedTab,
selectedTab: $selectedSidebarViewTab,
splitViewKind: splitViewKind,
tabViewBuilder: tabViewBuilder,
defaultContentBuilder: defaultContentBuilder,
Expand All @@ -96,14 +104,54 @@ public struct AdaptiveTabView<TabContent: Sequence, SidebarExtraContent: View, D
)
}
}
.onChange(of: horizontalSizeClass) { newValue in
guard let containerKind = newValue?.containerKind else { return }

selectedTab = selectedTabTransformer.transformer(containerKind, selectedTab)
switch containerKind {
case .tabView:
selectedTabViewTab = selectedTab
case .sidebarView:
selectedSidebarViewTab = selectedTab
}
}
.onChange(of: selectedTab) { newValue in
switch horizontalSizeClass {
case .compact:
guard selectedTabViewTab != newValue else { return }
selectedTabViewTab = newValue
default:
guard selectedSidebarViewTab != newValue else { return }
selectedSidebarViewTab = newValue
}
}
.onChange(of: selectedTabViewTab) { newValue in
guard selectedTab != newValue else { return }
selectedTab = newValue
}
.onChange(of: selectedSidebarViewTab) { newValue in
guard selectedTab != newValue else { return }
selectedTab = newValue
}
}
}

private extension UserInterfaceSizeClass {
var containerKind: AdaptiveTabViewContainerKind {
switch self {
case .compact: return .tabView
default: return .sidebarView
}
}
}

// MARK: - Previews

struct AdaptiveTabView_Previews: PreviewProvider {
@State static private var selectedTab = TabIdentifier("PreviewTitleImageProvidingView")

static var previews: some View {
AdaptiveTabView(appName: "AdaptiveTabView") { (_) in
AdaptiveTabView(appName: "AdaptiveTabView", selectedTab: $selectedTab) { (_) in
PreviewTitleImageProvidingView()
PreviewTitleImageProvidingView()
PreviewTitleImageProvidingView()
Expand Down
42 changes: 42 additions & 0 deletions Sources/AdaptiveTabView/Environment/SelectedTabTransformer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// SelectedTabTransformer.swift
//
//
// Created by Mark DiFranco on 2023-05-02.
//

import SwiftUI

/// A struct meant to hold transformation logic for the selected tab when the ``AdaptiveTabView`` switches between different ``AdaptiveTabViewContainerKind``s.
public struct SelectedTabTransformer {
/// A closure to use to transform ``TabIdentifier``s between the different ``AdaptiveTabViewContainerKind``s.
/// - parameter toKind: The container kind that the ``AdaptiveTabView`` will transform into.
/// - parameter tabIdentifier: The ``TabIdentifier`` currently selected in the previous ``AdaptiveTabViewContainerKind``.
public let transformer: (_ toKind: AdaptiveTabViewContainerKind, _ tabIdentifier: TabIdentifier) -> TabIdentifier

public init(transformer: @escaping (AdaptiveTabViewContainerKind, TabIdentifier) -> TabIdentifier) {
self.transformer = transformer
}
}

extension SelectedTabTransformer: EnvironmentKey {
public static var defaultValue = SelectedTabTransformer { (sizeClass, tabIdentifier) in
return tabIdentifier
}
}

public extension EnvironmentValues {
/// An environment value that holds logic for transforming the selected tab in an ``AdaptiveTabView``.
var selectedTabTransformer: SelectedTabTransformer {
get {self[SelectedTabTransformer.self]}
set {self[SelectedTabTransformer.self] = newValue}
}
}

public extension View {
/// Provide a transformer for converting the selected tab in an ``AdaptiveTabView`` when switching between ``AdaptiveTabViewContainerKind``s.
/// - parameter transformer: The transformer to use when converting the selected tab.
func selectedTabTransformer(_ transformer: SelectedTabTransformer) -> some View {
environment(\.selectedTabTransformer, transformer)
}
}
4 changes: 2 additions & 2 deletions Sources/AdaptiveTabView/TabLayout/TabLayoutView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ import SequenceBuilder

struct TabLayoutView<TabContent: Sequence>: View where TabContent.Element: TabContentView {

private let selectedTab: Binding<TabIdentifier>?
private let selectedTab: Binding<TabIdentifier>
private let tabViews: TabContent

init(
selectedTab: Binding<TabIdentifier>?,
selectedTab: Binding<TabIdentifier>,
@SequenceBuilder _ tabViewBuilder: (AdaptiveTabViewContainerKind) -> TabContent
) {
self.selectedTab = selectedTab
Expand Down

0 comments on commit 8b09eec

Please sign in to comment.