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:
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
iOS Is It a Static or a Dynamic Framework
Difference Between Static Function and Singleton Class in Swift
Autolayout, Unable to Simultaneously Satisfy Constraints
Creating a Percentage Based iOS Layout
How to Set Initial Values for Nsuserdefault Keys
Where and When to Get Data for Watch Complication
How to Use Networkreachabilitymanager in Alamofire
Styling the Cancel Button in a Uisearchbar
Fatal Error: Init(Coder:) Has Not Been Implemented Error Despite Being Implemented
Open Uisplitviewcontroller to Master View Rather Than Detail
App Store Connect Message: Your Account Will Soon Need to Be Migrated to Federated Auth
Why to Use Tuples When We Can Use Array to Return Multiple Values in Swift
Why Page Push Animation Tabbar Moving Up in the iPhone X
Navigation Pop View When Swipe Right Like Instagram iPhone App.How I Achieve This
Google Places Autocomplete on iOS - Can't Load Search Results - Try Again