SwiftUI将TupleView转换为AnyView数组

时间:2020-01-06 07:48:17

标签: swift swiftui

代码

我有以下代码:

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的数组?

2 个答案:

答案 0 :(得分:4)

这是我如何使用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)
        }
    }
}

我希望对大家都有用,如果您知道更好的方法,请告诉我!

答案 1 :(得分:0)

我为此创建了 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")
}