Swiftui Casting Tupleview to an Array of Anyview

SwiftUI Casting TupleView to an array of AnyView

Here's how I went about creating a custom tab view with 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])
}
}
}

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!

SwiftUI array of objects with AnyView

It is due to AnyView which disables automatic conforming to those protocols (because does not conform to them).

Here is possible solution:

struct DiscoverItem: Hashable, Equatable {
static func == (lhs: DiscoverItem, rhs: DiscoverItem) -> Bool {
lhs.id == rhs.id
}

func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(name)
}

let id = UUID()
var name: String
var destination: AnyView
}

How can I feed ForEach with items of TupleView in SwiftUI?

You can use my ViewExtractor package. Here is an example with your code:

struct CustomSpacerView: View {
private let views: [AnyView]

// For 2 or more views
init<Views>(@ViewBuilder content: TupleContent<Views>) {
views = ViewExtractor.getViews(from: content)
}

// For 0 or 1 view
init<Content: View>(@ViewBuilder content: NormalContent<Content>) {
views = ViewExtractor.getViews(from: content)
}

var body: some View {
VStack {
ForEach(views.indices) { index in
Spacer()

views[index]
}

Spacer()
}
}
}

This has the same usage in ContentView.

As an overview, this works by getting a Mirror of the content, converting the raw bytes to a 'fake' type representing a View, then convert this Any type to AnyView.

Is there any way to create/extract an array of Views using @ViewBuilder in SwiftUI

It's rare that you need to extract views from an array. If you are just looking to pass @ViewBuilder content into a view, you can simply do the following:

struct ContentView: View {
var body: some View {
VStackReplica {
Text("1st")
Text("2nd")
Text("3rd")
}
}
}

struct VStackReplica<Content: View>: View {
@ViewBuilder let content: () -> Content

var body: some View {
VStack(content: content)
}
}

If this isn't sufficient for your use-case, then see below.


I have got a generic version working, so there is no need to make multiple initializers for different lengths of tuples. In addition, the views can be anything you want (you are not restricted for every View to be the same type).

You can find a Swift Package I made for this at GeorgeElsham/ViewExtractor. That contains more than what's in this answer, because this answer is just a simplified & basic version. Since the code is slightly different to this answer, so read the README.md first for an example.

Back to the answer, example usage:

struct ContentView: View {

var body: some View {
LeaningTower {
Text("Something 1")
Text("Something 2")
Text("Something 3")
Image(systemName: "circle")
}
}
}

Definition of your view:

struct LeaningTower: View {
private let views: [AnyView]

init<Views>(@ViewBuilder content: @escaping () -> TupleView<Views>) {
views = content().getViews
}

var body: some View {
VStack {
ForEach(views.indices) { index in
views[index]
.offset(x: CGFloat(index * 30))
}
}
}
}

TupleView extension (AKA where all the magic happens):

extension TupleView {
var getViews: [AnyView] {
makeArray(from: value)
}

private struct GenericView {
let body: Any

var anyView: AnyView? {
AnyView(_fromValue: body)
}
}

private func makeArray<Tuple>(from tuple: Tuple) -> [AnyView] {
func convert(child: Mirror.Child) -> AnyView? {
withUnsafeBytes(of: child.value) { ptr -> AnyView? in
let binded = ptr.bindMemory(to: GenericView.self)
return binded.first?.anyView
}
}

let tupleMirror = Mirror(reflecting: tuple)
return tupleMirror.children.compactMap(convert)
}
}

Result:

Result

How to have a dynamic List of Views using SwiftUI

Looks like the answer was related to wrapping my view inside of AnyView

struct ContentView : View {
var myTypes: [Any] = [View1.self, View2.self]
var body: some View {
List {
ForEach(0..<myTypes.count) { index in
self.buildView(types: self.myTypes, index: index)
}
}
}

func buildView(types: [Any], index: Int) -> AnyView {
switch types[index].self {
case is View1.Type: return AnyView( View1() )
case is View2.Type: return AnyView( View2() )
default: return AnyView(EmptyView())
}
}
}

With this, i can now get view-data from a server and compose them. Also, they are only instanced when needed.



Related Topics



Leave a reply



Submit