ForEach with dynamic vs. static range - swiftui

I've been struggling with an-otherwise simple issue I'm sure. I can't seem to find the proper way to use ForEach with a dynamic range.
I have the following simple demo showing my issue. tapGesture on anySquare will update a #State variable array to make square 1 disappear / appear. Works like a charm on the right side but doesn't within a ForEach.
#State var visibilityArray = [true,true]
var body: some View {
ZStack {
Color.white
HStack {
VStack{
Text("Dynamic")
ForEach(visibilityArray.indices) { i in
if self.visibilityArray[i] {
Rectangle()
.frame(width: 100, height: 100)
.overlay(Text(String(i)).foregroundColor(Color.white))
.onTapGesture {
withAnimation(Animation.spring()) {
self.visibilityArray[1].toggle()
}
}
}
}
}
VStack{
Text("Static")
if self.visibilityArray[0] {
Rectangle()
.frame(width: 100, height: 100)
.overlay(Text("0").foregroundColor(Color.white))
.onTapGesture {
withAnimation(Animation.spring()) {
self.visibilityArray[1].toggle()
}
}
}
if self.visibilityArray[1] {
Rectangle()
.frame(width: 100, height: 100)
.overlay(Text("1").foregroundColor(Color.white))
.onTapGesture {
withAnimation(Animation.spring()) {
self.visibilityArray[1].toggle()
}
}
}
}
}
}
}

Xcode error occurring when you try to modify your array used in a ForEach loop (eg. by replacing it with another array) provides a good guidance:
ForEach(_:content:) should only be used for constant data.
Instead conform data to Identifiable or use ForEach(_:id:content:)
and provide an explicit id!
You can use this code to make your ForEach work:
ForEach(visibilityArray, id: \.self) {
This also means that for every item in the loop you have to return some View. I suggest passing the filtered array (only with items you want to display/use) as an input to the ForEach loop.

Related

Nesting ForEach loop and LazyVStack error

I have encountered two issues below my codes. They should be a simple fix but I couldn't figure them out by myself.
The first issue is I cannot pass my nested ForEach loop to change the system images. The reason I used for nested ForEach loop is the Xcode always throw the error "Expression was too complex to be solved in reasonable time". Here is the original link for this problem.
Nested If Statement in ForEach loop
import SwiftUI
struct HymnLyrics: View {
let verseImages: [String] = ["2.circle.fill", "3.circle.fill", "4.circle.fill", "5.circle.fill"]
var lyrics: [Lyric] = LyricList.hymnLa
#AppStorage("fontSizeIndex") var fontSizeIndex = Int("Medium") ?? 18
#AppStorage("fontIndex") var fontIndex: String = ""
#AppStorage("showHVNumbers") var showHVNumbers: Bool = true
#AppStorage("scrollToIndex") var scrollToIndex: Int?
var body: some View {
ScrollView {
ScrollViewReader { proxy in
LazyVStack(spacing: 10) {//LazyVStack is crash
ForEach (lyrics, id: \.id) { lyric in
Group {
HStack {
if showHVNumbers {
Image(systemName: "1.circle.fill")
.font(.title2)
.foregroundColor(.red)
}
Text(lyric.verse1)
.font(Font.custom(fontIndex, size: CGFloat(fontSizeIndex)))
Spacer()
}
ForEach ([lyric.verse2, lyric.verse3, lyric.verse4, lyric.verse5], id: \.self) { verseCount in
if (verseCount != nil) {
HStack {
if showHVNumbers {
Image(systemName: verseImages[verseCount])// Here is the error, it simply not works.
.font(.title2)
.foregroundColor(.red)
}
Text(verseCount ?? "")
.font(Font.custom(fontIndex, size: CGFloat(fontSizeIndex)))
Spacer()
}
}
}
}
.foregroundColor(.primary)
.padding(.horizontal, 10.0)
.padding(.bottom)
Divider()
}
.onChange(of: scrollToIndex, perform: { value in
proxy.scrollTo(value, anchor: .top)
})
}
}
}
}
}
The second issue is the app crashed when I scroll down to the bottom. When I replace LazyVStack with regular VStact, it works. But I want to use LazyVStack in this case. Please guide me for both of the issues. Thanks you in advance.

Is there a way in SwiftUI to fulfill scrolling animations between 2 numbers

I'm looking for a similar way https://github.com/stokatyan/ScrollCounter in SwiftUI
This is a very rudimentary build of the same thing. I'm not sure if you're wanting to do it on a per-digit basis, however this should give you a solid foundation to work off of. The way that I'm handling it is by using a geometry reader. You should be able to easily implement this view by utilizing an HStack for extra digits/decimals. The next thing I would do would be to create an extension that handles returning the views based on the string representation of your numeric value. Then that string is passed as an array and views created for each index in the array, returning a digit flipping view. You'd then have properties that are having their state observed, and change as needed. You can also attach an .opacity(...) modifier to give it that faded in/out look, then multiply the opacity * n where n is the animation duration.
In this example you can simply tie your Digit value to the previewedNumber and it should take over from there.
struct TestView: View {
#State var previewedNumber = 0;
var body: some View {
ZStack(alignment:.bottomTrailing) {
GeometryReader { reader in
VStack {
ForEach((0...9).reversed(), id: \.self) { i in
Text("\(i)")
.font(.system(size: 100))
.fontWeight(.bold)
.frame(width: reader.size.width, height: reader.size.height)
.foregroundColor(Color.white)
.offset(y: reader.size.height * CGFloat(previewedNumber))
.animation(.linear(duration: 0.2))
}
}.frame(width: reader.size.width, height: reader.size.height, alignment: .bottom)
}
.background(Color.black)
Button(action: {
withAnimation {
previewedNumber += 1
if (previewedNumber > 9) {
previewedNumber = 0
}
}
}, label: {
Text("Go To Next")
}).padding()
}
}
}

SwiftUI Frame width issue [duplicate]

I would like to underline a title with a rectangle that should have the same width as the Text.
First I create an underlined text as below:
struct Title: View {
var body: some View {
VStack {
Text("Statistics")
Rectangle()
.foregroundColor(.red)
.frame(height: (5.0))
}
}
}
So I get the following result:
Now I want to get this result:
So I would like to know if it's possible to bind Text width and apply it to Rectangle by writing something like :
struct Title: View {
var body: some View {
VStack {
Text("Statistics")
Rectangle()
.foregroundColor(.red)
.frame(width: Text.width, height: (5.0))
}
}
}
By doing so, I could change text and it will be dynamically underlined with correct width.
I tried many options but I can't find how to do it. I also checked this question but it's seems to not be the same issue.
Just specify that container has fixed size and it will tight to content, like
var body: some View {
VStack {
Text("Statistics")
Rectangle()
.foregroundColor(.red)
.frame(height: (5.0))
}.fixedSize() // << here !!
}

List view - any way to scroll horizontally?

I have a list that loads from a parsed CSV file built using SwiftUI and I can't seem to find a way to scroll the list horizontally.
List {
// Read each row of the array and return it as arrayRow
ForEach(arrayToUpload, id: \.self) { arrayRow in
HStack {
// Read each column of the array (Requires the count of the number of columns from the parsed CSV file - itemsInArray)
ForEach(0..<self.itemsInArray) { itemNumber in
Text(arrayRow[itemNumber])
.fixedSize()
.frame(width: 100, alignment: .leading)
}
}
}
}
.frame(minWidth: 1125, maxWidth: 1125, minHeight: 300, maxHeight: 300)
.border(Color.black)
The list renders how I would like but I'm just stuck on this one point.
Preview Image Of Layout
Swift 5;
iOS 13.4
You should use an ScrollView as Vyacheslav Pukhanov suggested but in your case the scrollView size does not get updated after the async call data arrive. So you have 2 options:
Provide a default value or an alternative view.
Provide a fixed size to the HStack inside of the ForeEach. (I used this one)
I faced the same problem laying out an horizontal grid of two columns. Here's my solution
import SwiftUI
struct ReviewGrid: View {
#ObservedObject private var reviewListViewModel: ReviewListViewModel
init(movieId: Int) {
reviewListViewModel = ReviewListViewModel(movieId: movieId)
//ReviewListViewModel will request all reviews for the given movie id
}
var body: some View {
let chunkedReviews = reviewListViewModel.reviews.chunked(into: 2)
// After the API call arrive chunkedReviews will get somethig like this => [[review1, review2],[review3, review4],[review5, review6],[review7, review8],[review9]]
return ScrollView (.horizontal) {
HStack {
ForEach(0..<chunkedReviews.count, id: \.self) { index in
VStack {
ForEach(chunkedReviews[index], id: \.id) { review in
Text("*\(review.body)*").padding().font(.title)
}
}
}
}
.frame(height: 200, alignment: .center)
.background(Color.red)
}
}
}
This is a dummy example don't expect a fancy view ;)
I hope it helps you.
You should use a horizontal ScrollView instead of the List for this purpose.
ScrollView(.horizontal) {
VStack {
ForEach(arrayToUpload, id: \.self) { arrayRow in
HStack {
ForEach(0..<self.itemsInArray) { itemNumber in
Text(arrayRow[itemNumber])
.fixedSize()
.frame(width: 100, alignment: .leading)
}
}
}
}
}

How to use GeometryReader to achieve a table like layout?

I'm trying to achieve a layout like this:
For this simple example the base would be something like this:
HStack {
VStack {
Text("Foo")
Text("W")
Text("X")
}
VStack {
Text("Bar")
Text("Y")
Text("Z")
}
}
Now that relativeSize(...) is deprecated, the only remaining option I see is GeometryReader, but the issue with it is that once it's itself nested in another stack, it will attempt to fill all available space, in other terms it cannot determine the size it's containing stack would have had if it wasn't present in it and I end up with an overly sized stack.
I wonder if I'm missing something or if this is just how stacks work, or maybe a beta bug?
Thank you for your help
EDIT:
I did this:
VStack {
GeometryReader { /* #kontiki code */ }
Text("Other")
Spacer().layoutPriority(1)
}
But unfortunately this is the result I get, do you think this is a SwiftUI bug?
Second Attempt
I think this does exactly what you need. It uses Preferences. If you need to learn more about how to use SwiftUI preferences, check this post I wrote. They are fully explained there, but it is too long of a subject to post it here.
import SwiftUI
struct MyPref: PreferenceKey {
typealias Value = CGFloat
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
}
struct SetWidthPreference: View {
var body: some View {
GeometryReader { proxy in
Rectangle().fill(Color.clear).preference(key: MyPref.self, value: proxy.size.width)
}
}
}
struct ContentView : View {
#State private var width: CGFloat = 0
var body: some View {
VStack {
ScrollView {
HStack(spacing: 0) {
VStack {
Text("Foo")
Text("Bar")
}.frame(width: width * 0.7, alignment: .leading).fixedSize().border(Color.red)
VStack {
Text("W")
Text("Y")
}.frame(width: width * 0.15).fixedSize().border(Color.red)
VStack {
Text("X")
Text("Z")
}.frame(width: width * 0.15).fixedSize().border(Color.red)
}
Text("Text below table")
}
.border(Color.green, width: 3)
HStack { Spacer() }.background(SetWidthPreference())
}
.onPreferenceChange(MyPref.self) { w in
print("\(w)")
DispatchQueue.main.async {
self.width = w
}
}
}
}
Previous Attempt (I keep it here, so comments make sense)
This example will draw 3 columns with 0.7, 0.15 and 0.15 of the parent's width. It's a starting point that you can fine tune. Note that the borders are there so that you can see what you are doing, of course you can remove them.
If GeometryReader is expanding too much, explain exactly what is that you want to accomplish, providing more context on the surroundings of the table (i.e., GeometryReader).
struct ContentView : View {
var body: some View {
GeometryReader { proxy in
HStack(spacing: 0) {
VStack {
Text("Foo")
Text("Bar")
}.frame(width: proxy.size.width * 0.7, alignment: .leading).fixedSize().border(Color.red)
VStack {
Text("W")
Text("Y")
}.frame(width: proxy.size.width * 0.15).fixedSize().border(Color.red)
VStack {
Text("X")
Text("Z")
}.frame(width: proxy.size.width * 0.15).fixedSize().border(Color.red)
}
}.padding(20)
}
}