SwiftUI将TupleView转换为AnyView数组

5vf7fwbs  于 2023-06-04  发布在  Swift
关注(0)|答案(3)|浏览(385)

代码

我有以下代码:

struct CustomTabView: View where Content: View {

    let children: [AnyView]

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

        let m = Mirror(reflecting: content())
        if let value = m.descendant("value") {
            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...
        }
    }
}

问题

我尝试将TupleView转换为AnyView数组,但收到错误

Protocol type 'Any' cannot conform to 'View' because only concrete types can conform to protocols

可能的解决方案

我可以解决这个问题的一种方法是将类型擦除的视图传入CustomTabView,如下所示:

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

理想情况下

但我希望能够像原生TabView一样执行以下操作

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

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

23c0lvtd

23c0lvtd1#

以下是我如何使用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 {
                // 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) -> CGFloat {
        // https://medium.com/@hacknicity/ipad-navigation-bar-and-toolbar-height-changes-in-ios-12-91c5766809f4
        // ipad 50
        // iphone && portrait 49
        // iphone && portrait && bottom safety 83
        // iphone && landscape 32
        // iphone && landscape && 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) -> some View {

        let tabBarHeight = getTabBarHeight(screenGeometry: screenGeometry)
        let heightCut = tabBarHeight - screenGeometry.safeAreaInsets.bottom
        let spacing: CGFloat = 100 // so pages don't overlap (in case of leading and trailing safetyInset), arbitrary

        return HStack(spacing: spacing) {
            self.content()
                // reduced height, so items don'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'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]) -> 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()
            }
            // move up from bottom safety inset
            .padding(.bottom, screenGeometry.safeAreaInsets.bottom > 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) -> 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)
                // 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])
        }
    }
}

以下是首选项和视图扩展:

// 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) -> 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 }
            value[0].tag = tag
            value[0].badgeNumber = badgeNumber
        }
        .tag(tag)
    }
}

用法如下:

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

我希望这对你们有用,如果你知道更好的方法,请告诉我!

abithluo

abithluo2#

我为此创建了IterableViewBuilder

struct ContentView: View {
...

  init<C: IterableView>(@IterableViewBuilder content: () -> C) {
    let count = content().count
    content().iterate(with: Visitor())
  }
}

struct Visitor: IterableViewVisitor {
  func visit<V>(_ value: V) where V : View {
    print("value")
  }
}
...

ContentView {
  Text("0")
  Text("1")
}
mrphzbgm

mrphzbgm3#

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

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

您需要实现其中一个协议。然后,在body(children: _VariadicView.Children) -> some View方法中,可以迭代所有子视图(_VariadicView.ChildrenCollection)。您的实现和子级应该使用_VariadicView.Tree视图 Package 。
两者之间的选择取决于您的需求:如果你想要一个像视图数组一样的视图,并且可以放在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()))
    }
}

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

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

相关问题