How to Have a Dynamic List of Views Using Swiftui

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.

SwiftUI: How to manage dynamic rows/columns of Views?

I think you're on the right track that you need to use an Identifiable to prevent the system from making assumptions about what can be recycled in the ForEach. To that end, I've created a Card:

struct Card : Identifiable {
let id = UUID()
var title : String
}

Within the RowView, this is trivial to use:

struct RowView: View {
var cards: [Card]
var width: CGFloat
var height: CGFloat
var columns: Int

var cardWidth: CGFloat {
return height * CardView.kPhysicalAspect
}

var cardSpacing: CGFloat {
return (width - cardWidth) / CGFloat(columns - 1)
}

var body: some View {
HStack(spacing: 0) {
// Visit all cards, but only add the ones that are within the range defined by start/columns
ForEach(cards) { card in
HStack(spacing: 0) {
CardView(faceCode: card.title)
.frame(width: cardWidth, height: height)
}
.frame(width: cardSpacing, alignment: .leading)
}
}
}
}

In the ContentView, things get a little more complicated because of the dynamic rows:

struct ContentView: View {
@State var cards: [Card] = (1..<53).map { Card(title: "\($0)") }
@State var fanned: Bool = true

// Generates the number of rows/columns that meets our rectangle-packing criteria
func pack(area: CGSize, count: Int) -> (rows: Int, cols: Int) {
let areaAspect = area.width / area.height
let exposureAspect = 1 - CardView.kVerticalCornerExposureRatio
let aspect = areaAspect / CardView.kPhysicalAspect * exposureAspect
var rows = Int(ceil(sqrt(Double(count)) / aspect))
let cols = count / rows + (count % rows > 0 ? 1 : 0)
while cols * (rows - 1) >= count { rows -= 1 }
return (rows, cols)
}

// Calculate the height of a card such that a series of rows overlap without covering the corner pips
func cardHeight(frameHeight: CGFloat, rows: Int) -> CGFloat {
let partials = CGFloat(rows - 1) * CardView.kVerticalCornerExposureRatio + 1
return frameHeight / partials
}

var body: some View {
VStack {
GeometryReader { geometry in
let w = geometry.size.width
let h = geometry.size.height
if w > 0 && h > 0 { // using `geometry.size != .zero` crashes the preview :(
let (rows, cols) = pack(area: geometry.size, count: cards.count)
let cardHeight = cardHeight(frameHeight: h, rows: rows)
let rowSpacing = cardHeight * CardView.kVerticalCornerExposureRatio

VStack(spacing: 0) {
ForEach(Array(cards.enumerated()), id: \.1.id) { (index, card) in
let row = index / cols
if index % cols == 0 {
let rangeMin = min(cards.count, row * cols)
let rangeMax = min(cards.count, rangeMin + cols)
RowView(cards: Array(cards[rangeMin..<rangeMax]), width: w, height: cardHeight, columns: cols)
.frame(width: w, height: rowSpacing, alignment: .topLeading)
}
}
}
.frame(width: w, height: 100, alignment: .topLeading)
}
}
}
}
}

This loops through all of the cards and uses the unique IDs. Then, there's some logic to use the index to determine what row the loop is on and if it is the beginning of the loop (and thus should render the row). Finally, it sends just a subset of the cards to the RowView.

Note: you can look at Swift Algorithms for a more efficient method than enumerated. See indexed: https://github.com/apple/swift-algorithms/blob/main/Guides/Indexed.md

Dynamic view rendering, SwiftUI

Here is possible approach in SwiftUI 2.0 (in SwiftUI 1.0 ViewBuilder does not support yet switch, so if/else is the only way, but you can wrap it in enum as well).

enum Choices: Int {
case menu = 1
case search = 2
case profile = 3
case settings = 4

@ViewBuilder
func view() -> some View {
switch self {
case menu:
MenuView()
case search:
SearchView()
case profile:
ProfileView()
case settings:
SettingsView()
}
}
}

struct DemoChoicesUsage: View {
@State var index = 1
var body: some View {
VStack {
(Choices(rawValue: index) ?? Choices.menu).view()
}
}
}

Also, optionally, you can modify also TopMenu to be linked directly to enum cases instead of indexes, like TopMenu(name: "Menu", tag: .menu)

struct TopMenu {
let name: String
let tag: Choices
}

SwiftUI - Is there any way to build a dynamic list view without NavigationView?

You could also use a modal presentation:

@State var selectedItem: Item? = nil

var body: some View {
List {
ForEach(items) { item in
WorkItemListRow(item: item)
.onTapGesture {
self.selectedItem = item
}
}
} .sheet(item: $selectedItem,
onDismiss: { self.selectedItem = nil }) { item in
WorkItemDetailView(item: item)
}
}
}

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

Swift UI - Dynamic List, TextField focus and deletion

I can replicate with your steps, I believe it is a bug.

You can circumvent the issue by using the "new" format and .number instead of formatter

TextField("field3", value: $line.field3, format: .number).textFieldStyle(.roundedBorder)
TextField("field4", value: $line.field4, format: .number).textFieldStyle(.roundedBorder)

You should submit a bug report

Working code

struct ContainerView: View {
@Binding var container: Container
var onRemove: (_ line: Line) -> Void
var body: some View {
ForEach($container.lines) { $line in
LineView(line: $line) {
onRemove(line)
}
}

}
}
struct LineView: View {
@Binding var line:Line
var onRemove: () -> Void

var body : some View {
HStack {
TextField("field1", text: $line.field1)
TextField("field2", text: $line.field2)
TextField("field3", value: $line.field3, format: .number)
TextField("field4", value: $line.field4, format: .number)
Button("remove") {
print("Remove insider")
onRemove()
}.buttonStyle(.bordered)
}.frame(maxWidth: 300)
.textFieldStyle(.roundedBorder)
}
}

Crash at a line

struct LineView: View {
@Binding var line:Line
var onRemove: () -> Void

var body : some View {
HStack {
TextField("field1", text: $line.field1)
TextField("field2", text: $line.field2)
TextField("field3", value:
Binding(get: {
line.field3 //**Crash at this line**
}, set: { new in
line.field3 = new
})
, formatter: .numberFormatter)
TextField("field4", value:
Binding(get: {
line.field4
}, set: { new in
line.field4 = new
}), formatter: .numberFormatter )
Button("remove") {
print("Remove insider")
onRemove()
}.buttonStyle(.bordered)
}.frame(maxWidth: 300)
.textFieldStyle(.roundedBorder)
}
}

extension Formatter{
static var numberFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
formatter.maximumFractionDigits = 4
return formatter
}()
}

WORKAROUND

Here is a workaround for now. It affects performance because it forces a full redraw of the View, you won't see much with a simple View like this but it will slow everything down if your views become longer and more complex.

Add .id(myContainer.lines.count) to the ContainerView

struct ContainerEditor: View {
@State var hidden = false
@State var myContainer = Container(lines: [
Line(field1: "Line1.1", field2: "Line1.2"),
Line(field1: "Line2.1", field2: "Line2.2"),
Line(field1: "Line3.1", field2: "Line3.2"),
Line(field1: "Line4.1", field2: "Line4.2"),
])
var body: some View {
if !hidden {
ContainerView(container: $myContainer) { line in
print("Removing:")
print(line)
myContainer.lines.removeAll(where: { $0.id == line.id })
}.id(myContainer.lines.count)

}
Button("Toggle hidden", action: { hidden = !hidden })
}
}


Related Topics



Leave a reply



Submit