英文:
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 () -> 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...
}
}
}
Problem
I'm trying to convert the TupleView
into an array of AnyView
but I'm receiving the error
Protocol type 'Any' cannot conform to 'View' 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("A"))
AnyView(Text("B"))
AnyView(Rectangle())
}
Ideally
but I'd like to be able to do the following just like the native TabView
CustomTabView {
Text("A")
Text("B")
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's how I went about creating a custom tab view with SwiftUI:
```swift
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])
}
}
}
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) -> 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)
}
}
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<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")
}
英文:
I created IterableViewBuilder
for this purpose
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")
}
答案3
得分: 0
实际上,有一种本地方法可以通过_VariadicView.MultiViewRoot
或_VariadicView.UnaryViewRoot
协议来实现这一点。
通常不鼓励使用内部API,因为它们可能在将来的更新中更改或删除,但这是唯一有效的方法。
您需要实现其中一个协议。然后,在body(children: _VariadicView.Children) -> some View
方法中,您可以遍历所有子视图(_VariadicView.Children
是一个Collection
)。您的实现和子视图应该包装在_VariadicView.Tree
视图中。
选择两者之间取决于您的需求:如果您想要一个表现得像一组视图并且可以放置在VStack
或HStack
等中的视图,您可以使用_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) -> 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<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()))
}
}
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)
}
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论