Reputation: 16730
I can do a static List like
List {
View1()
View2()
}
But how do i make a dynamic list of elements from an array? I tried the following but got error: Closure containing control flow statement cannot be used with function builder 'ViewBuilder'
let elements: [Any] = [View1.self, View2.self]
List {
ForEach(0..<elements.count) { index in
if let _ = elements[index] as? View1 {
View1()
} else {
View2()
}
}
}
Is there any work around for this? What I am trying to accomplish is a List contaning dynamic set of elements that are not statically entered.
Upvotes: 52
Views: 46481
Reputation: 2770
The problem with Paulo's example is you now have any type for your array. It's better to create a protocol for that type.
Here is my example that creates a list of folders and documents:
import SwiftUI
struct ContentView: View {
@State var listOfItems: [MultiTypeProtocol] = []
func createArray() -> Array<MultiTypeProtocol> {
let t1 = CardType(name: "name3")
let t2 = FolderType(name: "name1")
let t3 = FolderType(name: "name4")
let t4 = CardType(name: "name2")
return [t1, t2, t3, t4]
}
var body: some View {
VStack {
List(listOfItems, id: \.id) {obj in
switch obj.self {
case is CardType: AnyView(DocumentView(name: obj.name))
case is FolderType: AnyView(FolderView(name: obj.name))
default: AnyView(EmptyView())
}
}
}
.padding()
.onAppear {
listOfItems = createArray().shuffled()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct CardType: Identifiable, MultiTypeProtocol {
var id: String = UUID().uuidString
var name: String
}
struct FolderType: Identifiable, MultiTypeProtocol {
var id: String = UUID().uuidString
var name: String
}
protocol MultiTypeProtocol {
var id: String { get set }
var name: String { get set }
}
struct DocumentView: View {
var name: String = "Untitled"
var body: some View {
HStack{
Image(systemName: "doc")
.imageScale(.large)
.foregroundColor(.accentColor)
Text(name)
}
}
}
struct FolderView: View {
var name: String = "Untitled"
var body: some View {
VStack {
DisclosureGroup(
content: {Text(name)},
label: {
Image(systemName: "folder")
.imageScale(.large)
.foregroundColor(.accentColor)
Text(name)
}
)
}
}
}
So you create keep adding more objects to the list as long as they conform to MultiTypeProtocol.
Upvotes: 0
Reputation: 1
import SwiftUI
struct ContentView: View {
var animationList: [Any] = [
AnimationDemo.self, WithAnimationDemo.self, TransitionDemo.self
]
var body: some View {
NavigationView {
List {
ForEach(0..<animationList.count) { index in
NavigationLink(
destination: animationIndex(types: animationList, index: index),
label: {
listTitle(index: index)
})
}
}
.navigationBarTitle("Animations")
}
}
@ViewBuilder
func listTitle(index: Int) -> some View {
switch index {
case 0:
Text("AnimationDemo").font(.title2).bold()
case 1:
Text("WithAnimationDemo").font(.title2).bold()
case 2:
Text("TransitionDemo").font(.title2).bold()
default:
EmptyView()
}
}
@ViewBuilder
func animationIndex(types: [Any], index: Int) -> some View {
switch types[index].self {
case is AnimationDemo.Type:
AnimationDemo()
case is WithAnimationDemo.Type:
WithAnimationDemo()
case is TransitionDemo.Type:
TransitionDemo()
default:
EmptyView()
}
}
}
Upvotes: -1
Reputation: 21
Swift 5
this seems to work for me.
struct AMZ1: View {
var body: some View {
Text("Text")
}
}
struct PageView: View {
let elements: [Any] = [AMZ1(), AMZ2(), AMZ3()]
var body: some View {
TabView {
ForEach(0..<elements.count) { index in
if self.elements[index] is AMZ1 {
AMZ1()
} else if self.elements[index] is AMZ2 {
AMZ2()
} else {
AMZ3()
}
}
}
Upvotes: 1
Reputation: 54621
You can now use control flow statements directly in @ViewBuilder
blocks, which means the following code is perfectly valid:
struct ContentView: View {
let elements: [Any] = [View1.self, View2.self]
var body: some View {
List {
ForEach(0 ..< elements.count) { index in
if let _ = elements[index] as? View1 {
View1()
} else {
View2()
}
}
}
}
}
In addition to the accepted answer you can use @ViewBuilder
and avoid AnyView
completely:
@ViewBuilder
func buildView(types: [Any], index: Int) -> some View {
switch types[index].self {
case is View1.Type: View1()
case is View2.Type: View2()
default: EmptyView()
}
}
Upvotes: 2
Reputation: 16730
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.
Upvotes: 54
Reputation: 27224
You can do this by polymorphism:
struct View1: View {
var body: some View {
Text("View1")
}
}
struct View2: View {
var body: some View {
Text("View2")
}
}
class ViewBase: Identifiable {
func showView() -> AnyView {
AnyView(EmptyView())
}
}
class AnyView1: ViewBase {
override func showView() -> AnyView {
AnyView(View1())
}
}
class AnyView2: ViewBase {
override func showView() -> AnyView {
AnyView(View2())
}
}
struct ContentView: View {
let views: [ViewBase] = [
AnyView1(),
AnyView2()
]
var body: some View {
List(self.views) { view in
view.showView()
}
}
}
Upvotes: 5
Reputation: 852
I found a little easier way than the answers above.
Create your custom view.
Make sure that your view is Identifiable
(It tells SwiftUI it can distinguish between views inside the ForEach by looking at their id property)
For example, lets say you are just adding images to a HStack, you could create a custom SwiftUI View like:
struct MyImageView: View, Identifiable {
// Conform to Identifiable:
var id = UUID()
// Name of the image:
var imageName: String
var body: some View {
Image(imageName)
.resizable()
.frame(width: 50, height: 50)
}
}
Then in your HStack:
// Images:
HStack(spacing: 10) {
ForEach(images, id: \.self) { imageName in
MyImageView(imageName: imageName)
}
Spacer()
}
Upvotes: 3
Reputation: 559
You can use dynamic list of subviews, but you need to be careful with the types and the instantiation. For reference, this is a demo a dynamic 'hamburger' here, github/swiftui_hamburger.
// Pages View to select current page
/// This could be refactored into the top level
struct Pages: View {
@Binding var currentPage: Int
var pageArray: [AnyView]
var body: AnyView {
return pageArray[currentPage]
}
}
// Top Level View
/// Create two sub-views which, critially, need to be cast to AnyView() structs
/// Pages View then dynamically presents the subviews, based on currentPage state
struct ContentView: View {
@State var currentPage: Int = 0
let page0 = AnyView(
NavigationView {
VStack {
Text("Page Menu").color(.black)
List(["1", "2", "3", "4", "5"].identified(by: \.self)) { row in
Text(row)
}.navigationBarTitle(Text("A Page"), displayMode: .large)
}
}
)
let page1 = AnyView(
NavigationView {
VStack {
Text("Another Page Menu").color(.black)
List(["A", "B", "C", "D", "E"].identified(by: \.self)) { row in
Text(row)
}.navigationBarTitle(Text("A Second Page"), displayMode: .large)
}
}
)
var body: some View {
let pageArray: [AnyView] = [page0, page1]
return Pages(currentPage: self.$currentPage, pageArray: pageArray)
}
}
Upvotes: 5
Reputation: 120002
Is it possible to return different View
s based on needs?
In short: Sort of
As it's fully described in swift.org, It is IMPOSSIIBLE to have multiple Types returning as opaque type
If a function with an opaque return type returns from multiple places, all of the possible return values must have the same type. For a generic function, that return type can use the function’s generic type parameters, but it must still be a single type.
So how List
can do that when statically passed some different views?
List is not returning different types, it returns EmptyView
filled with some content view. The builder is able to build a wrapper around any type of view you pass to it, but when you use more and more views, it's not even going to compile at all! (try to pass more than 10 views for example and see what happens)
As you can see, List
contents are some kind of ListCoreCellHost
containing a subset of views that proves it's just a container of what it represents.
What if I have a lot of data, (like contacts) and want to fill a list for that?
You can conform to Identifiable
or use identified(by:)
function as described here.
What if any contact could have a different view?
As you call them contact, it means they are same thing! You should consider OOP to make them same and use inheritance
advantages. But unlike UIKit
, the SwiftUI
is based on structs. They can not inherit each other.
So what is the solution?
You MUST wrap all kind of views you want to display into the single View
type. The documentation for EmptyView
is not enough to take advantage of that (for now). BUT!!! luckily, you can use UIKit
How can I take advantage of UIKit
for this
View1
and View2
on top of UIKit
.ContainerView
with of UIKit.ContainerView
the way that takes argument and represent View1
or View2
and size to fit.UIViewRepresentable
and implement it's requirements.List
to show a list of ContainerView
So now it's a single type that can represent multiple viewsUpvotes: 0
Reputation: 22856
if/let
flow control statement cannot be used in a @ViewBuilder
block.
Flow control statements inside those special blocks are translated to structs.
e.g.
if (someBool) {
View1()
} else {
View2()
}
is translated to a ConditionalValue<View1, View2>
.
Not all flow control statements are available inside those blocks, i.e. switch
, but this may change in the future.
More about this in the function builder evolution proposal.
In your specific example you can rewrite the code as follows:
struct ContentView : View {
let elements: [Any] = [View1.self, View2.self]
var body: some View {
List {
ForEach(0..<elements.count) { index in
if self.elements[index] is View1 {
View1()
} else {
View2()
}
}
}
}
}
Upvotes: 7