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:
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
How to Prompt for Accessibility Features in a MACos App (From the Appdelegate)
How to Wait for Http Requests to Finish
Swift Realm: After Writing Transaction Reference Set to Nil
Saving Highscores with Nsuserdefaults
Get Compiler Error in Swift Indexof()
Value of Type 'Tags' Has No Member 'Lastused'
Get the First Day of Week Without Weekcalendarunit
Capturing a Struct Reference in a Closure Doesn't Allow Mutations to Occur
Hittest Prints Ar Entity Name Even When I am Not Tapping on It
Calculate the Number of Dimensions of a Multi-Dimensional Array in Swift
Swift Delegate Beetween Two Vc Without Segue
Swift Optionals - Why Does Var A:Int? A? = 4 Return Nil
How to Handle Parameter Validation Swift
Change Width of a Uibarbuttonitem in a Uinavigationbar in Swift