Skip to content

Commit

Permalink
Add new Tab elements and .sidebarAdaptable TabViewStyle (#1382)
Browse files Browse the repository at this point in the history
* Support deprecated modifiers (for backwards compatibility), and add new TabViewStyles

* Fix macOS build

* Exclude menuButtonStyle modifier

* Attempt to fix watchOS build
  • Loading branch information
carson-katri authored Oct 17, 2024
1 parent 2b54243 commit 394167c
Show file tree
Hide file tree
Showing 9 changed files with 2,263 additions and 772 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//
// AnyDisclosureGroupStyle+ParseableModifierValue.swift
//
//
// Created by Carson Katri on 6/20/24.
//

#if os(iOS) || os(macOS) || os(visionOS)
import SwiftUI
import LiveViewNativeStylesheet

/// See [`SwiftUI.DisclosureGroupStyle`](https://developer.apple.com/documentation/swiftui/DisclosureGroupStyle) for more details.
///
/// Possible values:
/// - `.automatic`
@_documentation(visibility: public)
enum AnyDisclosureGroupStyle: String, CaseIterable, ParseableModifierValue, DisclosureGroupStyle {
typealias _ParserType = ImplicitStaticMember<Self, EnumParser<Self>>

case automatic

func makeBody(configuration: Configuration) -> some View {
let disclosureGroup = SwiftUI.DisclosureGroup.init(isExpanded: configuration.$isExpanded) {
configuration.content
} label: {
configuration.label
}

switch self {
case .automatic:
disclosureGroup.disclosureGroupStyle(.automatic)
}
}
}
#endif
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// AnyNavigationViewStyle+ParseableModifierValue.swift
//
//
// Created by Carson Katri on 6/20/24.
//

import SwiftUI
import LiveViewNativeStylesheet

/// See [`SwiftUI.NavigationViewStyle`](https://developer.apple.com/documentation/swiftui/NavigationViewStyle) for more details.
///
/// - Note: This type is deprecated and should no longer be used.
@_documentation(visibility: public)
enum AnyNavigationViewStyle: String, CaseIterable, ParseableModifierValue, NavigationViewStyle {
typealias _ParserType = ImplicitStaticMember<Self, EnumParser<Self>>

case __never

@ViewBuilder
func _body(configuration: _NavigationViewStyleConfiguration) -> some View {
fatalError("This type is deprecated and should no longer be used.")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import LiveViewNativeStylesheet
/// - `.automatic`
/// - `.page`
/// - `.verticalPage`
/// - `.sidebarAdaptable`
///
/// ### Page Style
/// Pass an ``SwiftUI/IndexDisplayMode`` to customize this style.
Expand All @@ -39,6 +40,9 @@ enum AnyTabViewStyle: ParseableModifierValue {
#if os(watchOS)
case verticalPage(transitionStyle: Any?)
#endif
#if os(iOS) || os(macOS) || os(tvOS) || os(visionOS)
case sidebarAdaptable
#endif

static func parser(in context: ParseableModifierContext) -> some Parser<Substring.UTF8View, Self> {
ImplicitStaticMember {
Expand All @@ -64,6 +68,9 @@ enum AnyTabViewStyle: ParseableModifierValue {
}
})
#endif
#if os(iOS) || os(macOS) || os(tvOS) || os(visionOS)
ConstantAtomLiteral("sidebarAdaptable").map({ Self.sidebarAdaptable })
#endif
}
}
}
Expand Down Expand Up @@ -123,6 +130,14 @@ extension View {
self
}
#endif
#if os(iOS) || os(macOS) || os(tvOS) || os(visionOS)
case .sidebarAdaptable:
if #available(iOS 18.0, macOS 15.0, tvOS 18.0, visionOS 2.0, *) {
self.tabViewStyle(.sidebarAdaptable)
} else {
self
}
#endif
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// UITextAutocapitalizationType+ParseableModifierValue.swift
//
//
// Created by Carson Katri on 6/20/24.
//

#if os(iOS) || os(tvOS) || os(visionOS)
import SwiftUI
import LiveViewNativeStylesheet

extension UITextAutocapitalizationType: ParseableModifierValue {
public static func parser(in context: ParseableModifierContext) -> some Parser<Substring.UTF8View, Self> {
ImplicitStaticMember([
"none": Self.none,
"sentences": Self.sentences,
"words": Self.words,
"allCharacters": Self.allCharacters,
])
}
}
#endif
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
//

import SwiftUI
import LiveViewNativeCore
import OSLog

private let logger = Logger(subsystem: "LiveViewNative", category: "TabView")

/// Container that presents content on separate pages.
///
Expand Down Expand Up @@ -36,10 +40,162 @@ struct TabView<Root: RootRegistry>: View {
@LiveAttribute(.init(name: "phx-change")) var changeAttribute: String?

var body: some View {
SwiftUI.TabView(
selection: (selectionAttribute != nil || changeAttribute != nil) ? $selection : nil
) {
$liveElement.children()
if #available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *),
$liveElement.childNodes.contains(where: {
guard case let .element(element) = $0.data else { return false }
return element.tag == "Tab"
})
{
if selectionAttribute != nil || changeAttribute != nil {
SwiftUI.TabView(selection: $selection) {
TabTreeBuilder<Root, String?>().fromNodes($liveElement.childNodes, context: $liveElement.context.storage)
}
} else {
SwiftUI.TabView {
TabTreeBuilder<Root, Never>().fromNodes($liveElement.childNodes, context: $liveElement.context.storage)
}
}
} else {
SwiftUI.TabView(
selection: (selectionAttribute != nil || changeAttribute != nil) ? $selection : nil
) {
$liveElement.children()
}
}
}
}

/// A builder for `TabContent`.
@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
struct TabTreeBuilder<Root: RootRegistry, TabValue: Hashable> {
func fromNodes<Nodes>(_ nodes: Nodes, context: LiveContextStorage<Root>) -> some TabContent<TabValue>
where Nodes: RandomAccessCollection, Nodes.Index == Int, Nodes.Element == Node
{
ForEach(nodes, id: \.id) { node in
fromNode(node, context: context)
}
}

@TabContentBuilder<TabValue>
fileprivate func fromNode(_ node: Node, context: LiveContextStorage<Root>) -> some TabContent<TabValue> {
// ToolbarTreeBuilder.fromNode may not be called with a root or leaf node
if case .element(let element) = node.data {
Self.lookup(ElementNode(node: node, data: element))
}
}

@TabContentBuilder<TabValue>
static func lookup(_ node: ElementNode) -> some TabContent<TabValue> {
if node.tag == "Tab" {
Tab<Root, TabValue>(element: node)
} else if node.tag == "TabSection" {
Tab<Root, TabValue>(element: node) // todo
}
}
}

@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
struct Tab<Root: RootRegistry, TabValue: Hashable>: TabContent {
@ObservedElement private var element: ElementNode
@LiveContext<Root> private var context

init(element: ElementNode) {
self._element = .init(element: element)
}

var body: some TabContent<TabValue> {
if TabValue.self == Never.self {
noValueTab
} else if TabValue.self == String?.self {
stringValueTab
}
}

@TabContentBuilder<TabValue>
var noValueTab: some TabContent<TabValue> {
if let title,
let image
{
SwiftUI.Tab(title, image: image, role: role) {
content
}
.forceCast(as: TabValue.self)
} else if let title,
let systemImage
{
SwiftUI.Tab(title, systemImage: systemImage, role: role) {
content
}
.forceCast(as: TabValue.self)
} else {
SwiftUI.Tab(role: role) {
content
} label: {
label
}
.forceCast(as: TabValue.self)
}
}

@TabContentBuilder<TabValue>
var stringValueTab: some TabContent<TabValue> {
let value = try? element.attributeValue(String.self, for: "value")
if let title,
let image
{
SwiftUI.Tab(title, image: image, value: value, role: role) {
content
}
.forceCast(as: TabValue.self)
} else if let title,
let systemImage
{
SwiftUI.Tab(title, systemImage: systemImage, value: value, role: role) {
content
}
.forceCast(as: TabValue.self)
} else {
SwiftUI.Tab(value: value, role: role) {
content
} label: {
label
}
.forceCast(as: TabValue.self)
}
}

var title: String? { element.attributeValue(for: "title") }
var image: String? { element.attributeValue(for: "image") }
var systemImage: String? { element.attributeValue(for: "systemImage") }
var role: TabRole? { try? element.attributeValue(TabRole.self, for: "role") }

var content: some View {
context.buildChildren(of: element, forTemplate: "content", includeDefaultSlot: true)
}

var label: some View {
context.buildChildren(of: element, forTemplate: "label")
}
}

@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
extension SwiftUI.Tab {
func forceCast<V: Hashable>(as valueType: V.Type = V.self) -> SwiftUI.Tab<V, Content, Label> {
self as! SwiftUI.Tab<V, Content, Label>
}
}

@available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
extension TabRole: AttributeDecodable {
public init(from attribute: Attribute?, on element: ElementNode) throws {
guard let value = attribute?.value
else { throw AttributeDecodingError.missingAttribute(Self.self) }

switch value {
case "search":
self = .search
default:
throw AttributeDecodingError.badValue(Self.self)
}
}
}
Loading

0 comments on commit 394167c

Please sign in to comment.