SwiftUI将TupleView转换为AnyView数组

huangapple go评论72阅读模式
英文:

SwiftUI Casting TupleView to an array of AnyView

问题

// 问题
我尝试将`TupleView`转换为`AnyView`数组,但我收到以下错误消息

协议类型'Any'不能符合'View',因为只有具体类型才能符合协议


// 可能的解决方案
我可以通过以下方式解决这个问题:
```swift
CustomTabView {
    AnyView(Text("A"))
    AnyView(Text("B"))
    AnyView(Rectangle())
}

// 理想情况下
但我希望能够像原生的TabView一样执行以下操作:

CustomTabView {
    Text("A")
    Text("B")
    Rectangle()
}

那么,我应该如何将TupleView转换为AnyView数组?


<details>
<summary>英文:</summary>

# Code
I have the following code:

```swift
struct CustomTabView: View where Content: View {

    let children: [AnyView]

    init(@ViewBuilder content: @escaping () -&gt; Content) {
        self.content = content
        
        let m = Mirror(reflecting: content())
        if let value = m.descendant(&quot;value&quot;) {
            let tupleMirror = Mirror(reflecting: value)
            let tupleElements = tupleMirror.children.map({ AnyView($0.value) }) // ERROR
            self.children = tupleElements
        } else {
            self.children = [AnyView]()
        }
    }

    var body: some View {
        ForEach(self.children) { child in
            child...
        }
    }
}

Problem

I'm trying to convert the TupleView into an array of AnyView but I'm receiving the error

Protocol type &#39;Any&#39; cannot conform to &#39;View&#39; because only concrete types can conform to protocols

Possible solution

One way I can solve this is to pass in type erased views into CustomTabView like so:

CustomTabView {
    AnyView(Text(&quot;A&quot;))
    AnyView(Text(&quot;B&quot;))
    AnyView(Rectangle())
}

Ideally

but I'd like to be able to do the following just like the native TabView

CustomTabView {
    Text(&quot;A&quot;)
    Text(&quot;B&quot;)
    Rectangle()
}

So how would I go about converting the TupleView into an array of AnyView?

答案1

得分: 7

以下是使用SwiftUI创建自定义标签视图的代码部分的翻译:

struct CustomTabView<Content>: View where Content: View {

    @State private var currentIndex: Int = 0
    @EnvironmentObject private var model: Model

    let content: () -> Content

    init(@ViewBuilder content: @escaping () -> Content) {
        self.content = content
    }

    var body: some View {
        
        GeometryReader { geometry in
            return ZStack {
                // 页面
                // 所有页面上的onAppear仅在初始加载时调用
                self.pagesInHStack(screenGeometry: geometry)
            }
            .overlayPreferenceValue(CustomTabItemPreferenceKey.self) { preferences in
                // 标签栏
                return self.createTabBar(screenGeometry: geometry, tabItems: preferences.map {TabItem(tag: $0.tag, tab: $0.item)})
            }
        }
    }
    
    func getTabBarHeight(screenGeometry: GeometryProxy) -> CGFloat {
        // https://medium.com/@hacknicity/ipad-navigation-bar-and-toolbar-height-changes-in-ios-12-91c5766809f4
        // iPad 50
        // iPhone && 竖屏 49
        // iPhone && 竖屏 && 底部安全区域 83
        // iPhone && 横屏 32
        // iPhone && 横屏 && 底部安全区域 53
        if UIDevice.current.userInterfaceIdiom == .pad {
            return 50 + screenGeometry.safeAreaInsets.bottom
        } else if UIDevice.current.userInterfaceIdiom == .phone {
            if !model.landscape {
                return 49 + screenGeometry.safeAreaInsets.bottom
            } else {
                return 32 + screenGeometry.safeAreaInsets.bottom
            }
        }
        return 50
    }
    
    func pagesInHStack(screenGeometry: GeometryProxy) -> some View {
        
        let tabBarHeight = getTabBarHeight(screenGeometry: screenGeometry)
        let heightCut = tabBarHeight - screenGeometry.safeAreaInsets.bottom
        let spacing: CGFloat = 100 // 以防止页面重叠(如果有导航安全区域的情况下),任意选择
        
        return HStack(spacing: spacing) {
            self.content()
                // 减小高度,以防止项目出现在标签栏下方
                .frame(width: screenGeometry.size.width, height: screenGeometry.size.height - heightCut)
                // 上移以覆盖减小的高度
                // 为了iPhone X的导航栏颜色延伸到状态栏,加上0.1
                .offset(y: -heightCut/2 - 0.1)
        }
        .frame(width: screenGeometry.size.width, height: screenGeometry.size.height, alignment: .leading)
        .offset(x: -CGFloat(self.currentIndex) * screenGeometry.size.width + -CGFloat(self.currentIndex) * spacing)
    }
    
    func createTabBar(screenGeometry: GeometryProxy, tabItems: [TabItem]) -> some View {
        
        let height = getTabBarHeight(screenGeometry: screenGeometry)
        
        return VStack {
            Spacer()
            HStack(spacing: screenGeometry.size.width / (CGFloat(tabItems.count + 1) + 0.5)) {
                Spacer()
                ForEach(0..<tabItems.count, id: \.self) { i in
                    Group {
                        Button(action: {
                            self.currentIndex = i
                        }) {
                            tabItems[i].tab
                        }.foregroundColor(self.currentIndex == i ? .blue : .gray)
                    }
                }
                Spacer()
            }
            // 从底部安全区域向上移动
            .padding(.bottom, screenGeometry.safeAreaInsets.bottom > 0 ? screenGeometry.safeAreaInsets.bottom - 5 : 0 )
            .frame(width: screenGeometry.size.width, height: height)
            .background(
                self.getTabBarBackground(screenGeometry: screenGeometry)
            )
        }
        // 向下移动以覆盖新iPhone和iPad的底部
        .offset(y: screenGeometry.safeAreaInsets.bottom)
    }
    
    func getTabBarBackground(screenGeometry: GeometryProxy) -> some View {

        return GeometryReader { tabBarGeometry in
            self.getBackgrounRectangle(tabBarGeometry: tabBarGeometry)
        }
    }
    
    func getBackgrounRectangle(tabBarGeometry: GeometryProxy) -> some View {

        return VStack {
            Rectangle()
                .fill(Color.white)
                .opacity(0.8)
                // 顶部边框
                // https://www.reddit.com/r/SwiftUI/comments/dehx9t/how_to_add_border_only_to_bottom/
                .padding(.top, 0.2)
                .background(Color.gray)

                .edgesIgnoringSafeArea([.leading, .trailing])
        }
    }
}

以下是首选项和视图扩展的部分的翻译:

// MARK: - Tab Item Preference
struct CustomTabItemPreferenceData: Equatable {
    var tag: Int
    let item: AnyView
    let stringDescribing: String // 以便让首选项知道何时更改标签项
    var badgeNumber: Int // 以便让首选项知道何时更改badgeNumber
    

    static func == (lhs: CustomTabItemPreferenceData, rhs: CustomTabItemPreferenceData) -> Bool {
        lhs.tag == rhs.tag && lhs.stringDescribing == rhs.stringDescribing && lhs.badgeNumber == rhs.badgeNumber
    }
}

struct CustomTabItemPreferenceKey: PreferenceKey {

    typealias Value = [CustomTabItemPreferenceData]

    static var defaultValue: [CustomTabItemPreferenceData] = []

    static func reduce(value: inout [CustomTabItemPreferenceData], nextValue: () -> [CustomTabItemPreferenceData]) {
        value.append(contentsOf: nextValue())
    }
}

// TabItem
extension View {
    func customTabItem<Content>(@ViewBuilder content: @escaping () -> Content) -> some View where Content: View {
        self.preference(key: CustomTabItemPreferenceKey.self, value: [
            CustomTabItemPreferenceData(tag: 0, item: AnyView(content()), stringDescribing: String(describing: content()), badgeNumber: 0)
        ])
    }
}

// Tag
extension View {
    func customTag(_ tag: Int, badgeNumber: Int = 0) -> some View {

        self.transformPreference(CustomTabItemPreferenceKey.self) { (value: inout [CustomTabItemPreferenceData]) in

            guard value.count > 0 else { return }
            value[0].tag = tag
            value[0].badgeNumber = badgeNumber

        }
        .transformPreference(CustomTabItemPreferenceKey.self) { (value: inout [CustomTabItemPreferenceData]) -> Void in

            guard value.count > 0 else { return

<details>
<summary>英文:</summary>

Here&#39;s how I went about creating a custom tab view with SwiftUI:

```swift
struct CustomTabView&lt;Content&gt;: View where Content: View {

    @State private var currentIndex: Int = 0
    @EnvironmentObject private var model: Model

    let content: () -&gt; Content

    init(@ViewBuilder content: @escaping () -&gt; Content) {
        self.content = content
    }

    var body: some View {
        
        GeometryReader { geometry in
            return ZStack {
                // pages
                // onAppear on all pages are called only on initial load
                self.pagesInHStack(screenGeometry: geometry)
            }
            .overlayPreferenceValue(CustomTabItemPreferenceKey.self) { preferences in
                // tab bar
                return self.createTabBar(screenGeometry: geometry, tabItems: preferences.map {TabItem(tag: $0.tag, tab: $0.item)})
            }
        }
    }
    
    func getTabBarHeight(screenGeometry: GeometryProxy) -&gt; CGFloat {
        // https://medium.com/@hacknicity/ipad-navigation-bar-and-toolbar-height-changes-in-ios-12-91c5766809f4
        // ipad 50
        // iphone &amp;&amp; portrait 49
        // iphone &amp;&amp; portrait &amp;&amp; bottom safety 83
        // iphone &amp;&amp; landscape 32
        // iphone &amp;&amp; landscape &amp;&amp; bottom safety 53
        if UIDevice.current.userInterfaceIdiom == .pad {
            return 50 + screenGeometry.safeAreaInsets.bottom
        } else if UIDevice.current.userInterfaceIdiom == .phone {
            if !model.landscape {
                return 49 + screenGeometry.safeAreaInsets.bottom
            } else {
                return 32 + screenGeometry.safeAreaInsets.bottom
            }
        }
        return 50
    }
    
    func pagesInHStack(screenGeometry: GeometryProxy) -&gt; some View {
        
        let tabBarHeight = getTabBarHeight(screenGeometry: screenGeometry)
        let heightCut = tabBarHeight - screenGeometry.safeAreaInsets.bottom
        let spacing: CGFloat = 100 // so pages don&#39;t overlap (in case of leading and trailing safetyInset), arbitrary
        
        return HStack(spacing: spacing) {
            self.content()
                // reduced height, so items don&#39;t appear under tha tab bar
                .frame(width: screenGeometry.size.width, height: screenGeometry.size.height - heightCut)
                // move up to cover the reduced height
                // 0.1 for iPhone X&#39;s nav bar color to extend to status bar
                .offset(y: -heightCut/2 - 0.1)
        }
        .frame(width: screenGeometry.size.width, height: screenGeometry.size.height, alignment: .leading)
        .offset(x: -CGFloat(self.currentIndex) * screenGeometry.size.width + -CGFloat(self.currentIndex) * spacing)
    }
    
    func createTabBar(screenGeometry: GeometryProxy, tabItems: [TabItem]) -&gt; some View {
        
        let height = getTabBarHeight(screenGeometry: screenGeometry)
        
        return VStack {
            Spacer()
            HStack(spacing: screenGeometry.size.width / (CGFloat(tabItems.count + 1) + 0.5)) {
                Spacer()
                ForEach(0..&lt;tabItems.count, id: \.self) { i in
                    Group {
                        Button(action: {
                            self.currentIndex = i
                        }) {
                            tabItems[i].tab
                        }.foregroundColor(self.currentIndex == i ? .blue : .gray)
                    }
                }
                Spacer()
            }
            // move up from bottom safety inset
            .padding(.bottom, screenGeometry.safeAreaInsets.bottom &gt; 0 ? screenGeometry.safeAreaInsets.bottom - 5 : 0 )
            .frame(width: screenGeometry.size.width, height: height)
            .background(
                self.getTabBarBackground(screenGeometry: screenGeometry)
            )
        }
        // move down to cover bottom of new iphones and ipads
        .offset(y: screenGeometry.safeAreaInsets.bottom)
    }
    
    func getTabBarBackground(screenGeometry: GeometryProxy) -&gt; some View {

        return GeometryReader { tabBarGeometry in
            self.getBackgrounRectangle(tabBarGeometry: tabBarGeometry)
        }
    }
    
    func getBackgrounRectangle(tabBarGeometry: GeometryProxy) -&gt; some View {

        return VStack {
            Rectangle()
                .fill(Color.white)
                .opacity(0.8)
                // border top
                // https://www.reddit.com/r/SwiftUI/comments/dehx9t/how_to_add_border_only_to_bottom/
                .padding(.top, 0.2)
                .background(Color.gray)

                .edgesIgnoringSafeArea([.leading, .trailing])
        }
    }
}

Here's the preference and view extensions:

// MARK: - Tab Item Preference
struct CustomTabItemPreferenceData: Equatable {
    var tag: Int
    let item: AnyView
    let stringDescribing: String // to let preference know when the tab item is changed
    var badgeNumber: Int // to let preference know when the badgeNumber is changed
    

    static func == (lhs: CustomTabItemPreferenceData, rhs: CustomTabItemPreferenceData) -&gt; Bool {
        lhs.tag == rhs.tag &amp;&amp; lhs.stringDescribing == rhs.stringDescribing &amp;&amp; lhs.badgeNumber == rhs.badgeNumber
    }
}

struct CustomTabItemPreferenceKey: PreferenceKey {

    typealias Value = [CustomTabItemPreferenceData]

    static var defaultValue: [CustomTabItemPreferenceData] = []

    static func reduce(value: inout [CustomTabItemPreferenceData], nextValue: () -&gt; [CustomTabItemPreferenceData]) {
        value.append(contentsOf: nextValue())
    }
}

// TabItem
extension View {
    func customTabItem&lt;Content&gt;(@ViewBuilder content: @escaping () -&gt; Content) -&gt; some View where Content: View {
        self.preference(key: CustomTabItemPreferenceKey.self, value: [
            CustomTabItemPreferenceData(tag: 0, item: AnyView(content()), stringDescribing: String(describing: content()), badgeNumber: 0)
        ])
    }
}

// Tag
extension View {
    func customTag(_ tag: Int, badgeNumber: Int = 0) -&gt; some View {

        self.transformPreference(CustomTabItemPreferenceKey.self) { (value: inout [CustomTabItemPreferenceData]) in

            guard value.count &gt; 0 else { return }
            value[0].tag = tag
            value[0].badgeNumber = badgeNumber

        }
        .transformPreference(CustomTabItemPreferenceKey.self) { (value: inout [CustomTabItemPreferenceData]) -&gt; Void in

            guard value.count &gt; 0 else { return }
            value[0].tag = tag
            value[0].badgeNumber = badgeNumber
        }
        .tag(tag)
    }
}

and here's the usage:

struct MainTabsView: View {
    var body: some View {
        // TabView
        CustomTabView {
            A()
                .customTabItem { ... }
                .customTag(0, badgeNumber: 1)
            B()
                .customTabItem { ... }
                .customTag(2)
            C()
                .customTabItem { ... }
                .customTag(3)
        }
    }
}

I hope that's useful to y'all out there, let me know if you know a better way!

答案2

得分: 2

我为此目的创建了 IterableViewBuilder

struct ContentView: View {
...
init&lt;C: IterableView&gt;(@IterableViewBuilder content: () -&gt; C) {
let count = content().count
content().iterate(with: Visitor())
}
}
struct Visitor: IterableViewVisitor {
func visit&lt;V&gt;(_ value: V) where V : View {
print(&quot;value&quot;)
}
}
...	
ContentView {
Text(&quot;0&quot;)
Text(&quot;1&quot;)
}
英文:

I created IterableViewBuilder for this purpose

struct ContentView: View {
...
init&lt;C: IterableView&gt;(@IterableViewBuilder content: () -&gt; C) {
let count = content().count
content().iterate(with: Visitor())
}
}
struct Visitor: IterableViewVisitor {
func visit&lt;V&gt;(_ value: V) where V : View {
print(&quot;value&quot;)
}
}
...
ContentView {
Text(&quot;0&quot;)
Text(&quot;1&quot;)
}

答案3

得分: 0

实际上,有一种本地方法可以通过_VariadicView.MultiViewRoot_VariadicView.UnaryViewRoot协议来实现这一点。

通常不鼓励使用内部API,因为它们可能在将来的更新中更改或删除,但这是唯一有效的方法。

您需要实现其中一个协议。然后,在body(children: _VariadicView.Children) -> some View方法中,您可以遍历所有子视图(_VariadicView.Children是一个Collection)。您的实现和子视图应该包装在_VariadicView.Tree视图中。

选择两者之间取决于您的需求:如果您想要一个表现得像一组视图并且可以放置在VStackHStack等中的视图,您可以使用_VariadicView.MultiViewRoot。另一方面,如果您需要一个独立的单一视图,_VariadicView.UnaryViewRoot将是一种选择。

这里是一个例子:

public struct WithSeparator<Separator: View>: ViewModifier {

    public var separator: Separator

    public func body(content: Content) -> some View {
        _VariadicView.Tree(Root(base: self)) {
            content
        }
    }

    private struct Root: _VariadicView.MultiViewRoot {

        let base: WithSeparator
        @Environment(\.separatorLocation)
        private var separatorLocation

        func body(children: _VariadicView.Children) -> some View {
            if !children.isEmpty {
                if separatorLocation.contains(.start) {
                    base.separator
                }
                ForEach(Array(children.dropLast())) { child in
                    child
                    if separatorLocation.contains(.between) {
                        base.separator
                    }
                }
                children[children.count - 1]
                if separatorLocation.contains(.end) {
                    base.separator
                }
            }
        }
    }
}

public extension View {

    func separator(@ViewBuilder _ separator: () -> some View) -> some View {
        modifier(WithSeparator(separator: separator()))
    }
}

这段代码创建了一个WithSeparator结构体,它使用了_VariadicView.MultiViewRoot协议。这允许您在视图之间添加一个分隔符。

VStack {
   ForEach(...) { 
      ...
   }.separator {
      Color.black.frame(height: 1)
   }
}
英文:

Actually, there's a native approach to accomplish this via the _VariadicView.MultiViewRoot or _VariadicView.UnaryViewRoot protocols.

Using internal APIs is generally discouraged because they can change or be removed in future updates, but it's the only approach that works well.

You need to implement one of these protocols. Then, in the body(children: _VariadicView.Children) -&gt; some View method, you can iterate through all subviews (_VariadicView.Children is a Collection). Your implementation and children should be wrapped with the _VariadicView.Tree view.

The choice between the two depends on your needs: if you want a view that behaves like an array of views and can be placed in a VStack or HStack etc, you would use _VariadicView.MultiViewRoot. On the other hand, if you need a single standalone view, _VariadicView.UnaryViewRoot would be the way to go.

Here's an example:

public struct WithSeparator&lt;Separator: View&gt;: ViewModifier {

    public var separator: Separator

    public func body(content: Content) -&gt; some View {
        _VariadicView.Tree(Root(base: self)) {
            content
        }
    }

    private struct Root: _VariadicView.MultiViewRoot {

        let base: WithSeparator
        @Environment(\.separatorLocation)
        private var separatorLocation

        func body(children: _VariadicView.Children) -&gt; some View {
            if !children.isEmpty {
                if separatorLocation.contains(.start) {
                    base.separator
                }
                ForEach(Array(children.dropLast())) { child in
                    child
                    if separatorLocation.contains(.between) {
                        base.separator
                    }
                }
                children[children.count - 1]
                if separatorLocation.contains(.end) {
                    base.separator
                }
            }
        }
    }
}

public extension View {

    func separator(@ViewBuilder _ separator: () -&gt; some View) -&gt; some View {
        modifier(WithSeparator(separator: separator()))
    }
}

This code creates a WithSeparator struct that uses the _VariadicView.MultiViewRoot protocol. This allows you to add a separator between your views.

VStack {
   ForEach(...) { 
      ...
   }.separator {
      Color.black.frame(height: 1)
   }
}

huangapple
  • 本文由 发表于 2020年1月6日 15:48:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/59608417.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定