How to create views in SwiftUI using collect - swift

I am trying to figure out a way to insert multiple arrays of views in a VStack in SwiftUI using collect() operator.
struct ChatsTab: View {
var subscriptions = Set<AnyCancellable>()
var body: some View {
VStack {
["A", "B", "C", "D", "E"].publisher.collect(2).sink(receiveCompletion: { _ in
// Do nothing on completion
}) { (stringArray) in
HStack {
Text(stringArray[0])
Text(stringArray[1])
}
}
.store(in: &subscriptions)
}
}
}
But I'm getting this below error:
Cannot convert value of type '()' to closure result type '_'
I want to do this with collect only so I can add my text views in pair. I know I have other options but I want to get it done with collect only.

You just have to rearrange things. The real issue is not the collect, the issue is that you're trying to execute arbitrary code inside the VStack. The VStack is a function builder and between it's curly braces goes a list of views and possibly some basic if logic only. Not arbitrary code, like the kind you include in a regular function.
So if you take the publisher code out of the VStack it will compile. Where you put it is up to you, you can have it in init(), called from another function, or int didAppear view modifier (just not inside the VStack directly).
A second issue is that the collect will publish another array (one of length 2). You will essentially end up with an array of String arrays (or you can just consume them as they are published and not keep them around, depending on what you are trying to do). You can also use the simpler version of sink since the Error of the publisher here is Never. Anyway here is something incorporating the above changes:
import SwiftUI
import Combine
var subscriptions = Set<AnyCancellable>()
struct ContentView: View {
#State var stringArrays: [[String]] = []
var body: some View {
ForEach(stringArrays, id: \.self) { stringArray in
HStack {
Text(stringArray.count > 0 ? stringArray[0] : "")
Text(stringArray.count > 1 ? stringArray[1] : "")
}
}.onAppear() {
["A", "B", "C", "D", "E"].publisher
.collect(2)
.sink(receiveValue: { (stringArray) in
self.stringArrays.append(stringArray)
}).store(in: &subscriptions)
}
}
}

Related

SwiftUI ForEach index jump by 2

I am working on SwiftUI ForEach. Below image shows what I want to achieve. For this purpose I need next two elements of array in single iteration, so that I can show two card in single go. I search on a lot but did find any way to jump index swiftUI ForEach.
Need to show two cards in single iteration
Here is my code in which I have added the element same array for both card which needs to be in sequence.
struct ContentView: View {
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 0) {
// I need jump of 2 indexes
ForEach(videos) { video in
//need to show the next two elements of the videos array
HStack {
videoCardView(video: video)
Spacer()
//video + 1
videoCardView(video: video)
}
.padding([.leading, .trailing], 30)
.padding([.top, .bottom], 10)
}
}
}
.background(Color(ColorName.appBlack.rawValue))
}
}
Any better suggestion how to build this view.
While LazyVGrid is probably the best solution for what you want to accomplish, it doesn't actually answer your question.
To "jump" an index is usually referred to as "stepping" in many programming languages, and in Swift it's called "striding".
You can stride (jump) an array by 2 like this:
ForEach(Array(stride(from: 0, to: array.count, by: 2)), id: \.self) { index in
// ...
}
You can learn more by taking a look at the Strideable protocol.
ForEach isn’t a for loop, many make that mistake. You need to supply identifiable data to it which should give you the clue to get your data into a suitable format first. You could process the array into another array containing a struct that has an id and holds the first and second video and pass that to the View that does the ForEach. View structs are lightweight make as many as you need.
You could also make a computed var but that wouldn’t be as efficient as a separate View because you might unnecessary recompute if something different changes.
Foreach is constrained compared to a 'for' loop. One way to fool ForEach into behaving differently is to create a shadow array for ForEach to loop through.
My purpose was slightly different than yours, but the workaround below seems like it could solve your challenge as well.
import SwiftUI
let images = ["house", "gear", "car"]
struct ContentView: View {
var body: some View {
VStack {
let looper = createCounterArray()
ForEach (looper, id:\.self) { no in
Image(systemName: images[no])
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
}
}
}
//
// return an array of the simulated loop data.
//
func createCounterArray() -> [Int] {
// create the data needed
return Array(arrayLiteral: 0,1,1,2)
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How to perform `ForEach` on a computed property in SwiftUI

Intro
Imagine we have an infinite range like the range of all possible integers and we can NOT just store them in memory. So we need to calculate them chunk by chunk like:
func numbers(around number: Int, distance: Int) -> [Int] {
((number - distance)...(number + distance))
.map { $0 } // Using `map` ONLY for making the question clear that we do NOT want to use a range
}
so now we can build our view like:
struct ContentView: View {
#State var lastVisibleNumber: Int = 0
var body: some View {
ScrollView(.horizontal) {
LazyHStack {
ForEach(numbers(around: lastVisibleNumber, distance: 10), id:\.self) { number in
Text("\(number)")
.padding()
.foregroundColor(.white)
.background(Circle())
// .onAppear { lastVisibleNumber = number }
}
}
}
}
}
Describing the issue and what tried
In order to load more numbers when needed, I have tried to update the lastVisibleNumber on the appearance of last visible Number by adding the following modifier on the Text:
.onAppear { lastVisibleNumber = number }
Although there is a safe range that is visible and should prevent the infinite loop (theoretically), adding this modifier will freeze the view for calculating all numbers!
So how can we achieve a scroll view with some kind of infinite data source?
Considerations
The range of Int is just a sample and the question can be about an infinite range of anything. (In a form of an Array!)
we don't want unexpected stops (like showing a dummy spinner at the leading or trailing of the list) in the middle of the scrolling.
For this, you should try using table view and data source. Let's assume you have an array of integers. You may create a buffer with an arbitrary number of instances. Let say 100. In that case, with a similar logic, your distance would become a 50. When you scroll, and close enough to the limits, you create another array and reload the table view data source so that you can pretend like you have an infinite array. Be aware that reusable cell and table view implementation is very consistent and optimized.
Don’t refresh view when lastVisibleNumber changes - it just gets into endless loop. You can create Observableobject, store it and update it only on-demand (I provided a button but you can obviously load it onAppear eith some criteria - e.g. last of the array was shown):
class NumberViewModel: ObservableObject {
var lastVisibleNumber = 0
#Published var lastRefreshedNumber = 0
func numbers(distance: Int) -> [Int] {
((lastRefreshedNumber - distance)...(lastRefreshedNumber + distance))
.map { $0 } // Using `map` ONLY for making the question clear that we do NOT want to use a range
}
}
struct ContentView: View {
#StateObject var viewModel = NumberViewModel()
var body: some View {
VStack {
Button("Refresh view", action: {
viewModel.lastRefreshedNumber = viewModel.lastVisibleNumber
})
ScrollView(.horizontal) {
LazyHStack {
ForEach(viewModel.numbers(distance: 10), id:\.self) { number in
Text("\(number)")
.padding()
.foregroundColor(.white)
.background(Circle())
.onAppear { viewModel.lastVisibleNumber = number }
}
}
}
}
}
}

SwiftUI Add to ForEach array without causing the entire view to reload

if I have something like this:
struct ContentView: View {
var results = [Result(score: 8), Result(score: 5), Result(score: 10)]
var body: some View {
VStack {
ForEach(results, id: \.id) { result in
Text("Result: \(result.score)")
}
}
}
}
And then I have a button that appends sometihng to the results array, the entire ForEach loop will reload. This makes sense, but I'm wondering if there is some way to prevent this. The reason I'm asking is because I have a ForEach loop with a few items, each of which plays an animation. If another item is appended to the array, however, the new item appears at the top of the ForEach, but, since the entire view is reload, the other animations playing in the items stop.
Is there any way to prevent this? Like to add an item to a ForEach array, and have it appear, but not reload the entire ForEach loop?
I assume not, but I would wonder how to get around such an issue.
Create separate view for iterating ForEach content, then SwiftUI rendering engine will update container (by adding new item), but not refresh existed rows
ForEach(results, id: \.id) {
ResultCellView(result: $0)
}
and
struct ResultCellView: View {
let result: Result
var body: some View {
Text("Result: \(result.score)")
}
}
Note: I don't see your model, so there might be needed to confirm it to Hashable, Equatable.
In general not providing an id makes it impossible for the ForEach to know what changed (as it has no track of the items) and therefore does not re-render the view.
E.g.
struct ContentView: View {
#State var myData: Array<String> = ["first", "second"]
var body: some View {
VStack() {
ForEach(0..<self.myData.count) { item in
Text(self.myData[item])
}
Button(action: {
self.myData.append("third")
}){
Text("Add third")
}
}
}
}
This throws an console output (that you can ignore) where it tells you about what I just wrote above:
ForEach<Range<Int>, Int, Text> count (3) != its initial count (2).
`ForEach(_:content:)` should only be used for *constant* data.
Instead conform data to `Identifiable` or use `ForEach(_:id:content:)`
and provide an explicit `id`!
For your code try this:
Tested on iOS 13.5.
struct ContentView: View {
#State var results = [Result(score: 8), Result(score: 5), Result(score: 10)]
var body: some View {
VStack {
ForEach(0..<self.results.count) { item in
// please use some index test on production
Text("Result: \(self.results[item].score)")
}
Button(action: {
self.results.append(Result(score: 11))
}) {
Text("Add 11")
}
}
}
}
class Result {
var score: Int
init(score: Int) {
self.score = score
}
}
Please note that this is a "hacky" solution and ForEach was not intended to be used for such cases. (See the console output)

In SwiftUI, How can I change the way List stack the row?

As default SwiftUI List stack the row by time, which results in the most recently created row is at the end of the list.
Instead I want most recently created row to be at upfront, the first row of the list.
Is there anyway to achieve this?
You can reverse the List so the most recent data is stored on top.
Here is an example with a button so you can add elements to the List and test it out.
struct ContentView: View {
#State var dataStore = [0, 1, 2]
var body: some View {
VStack {
List(dataStore.reversed(), id: \.self) { data in
Text(String(data))
}
Button(action: {
self.dataStore += [self.dataStore.count]
}) {
Text("Insert to list")
}
}.listStyle(GroupedListStyle())
}
}

Using ForEach with a an array of Bindings (SwiftUI)

My objective is to dynamically generate a form from JSON. I've got everything put together except for generating the FormField views (TextField based) with bindings to a dynamically generated list of view models.
If I swap out the FormField views for just normal Text views it works fine (see screenshot):
ForEach(viewModel.viewModels) { vm in
Text(vm.placeholder)
}
for
ForEach(viewModel.viewModels) { vm in
FormField(viewModel: $vm)
}
I've tried to make the viewModels property of ConfigurableFormViewModel an #State var, but it loses its codability. JSON > Binding<[FormFieldViewModel] naturally doesn't really work.
Here's the gist of my code:
The first thing that you can try is this:
ForEach(0 ..< numberOfItems) { index in
HStack {
TextField("PlaceHolder", text: Binding(
get: { return items[index] },
set: { (newValue) in return self.items[index] = newValue}
))
}
}
The problem with the previous approach is that if numberOfItems is some how dynamic and could change because of an action of a Button for example, it is not going to work and it is going to throw the following error: ForEach<Range<Int>, Int, HStack<TextField<Text>>> count (3) != its initial count (0). 'ForEach(_:content:)' should only be used for *constant* data. Instead conform data to 'Identifiable' or use 'ForEach(_:id:content:)' and provide an explicit 'id'!
If you have that use case, you can do something like this, it will work even if the items are increasing or decreasing during the lifecycle of the SwiftView:
ForEach(items.indices, id:\.self ){ index in
HStack {
TextField("PlaceHolder", text: Binding(
get: { return items[index] },
set: { (newValue) in return self.items[index] = newValue}
))
}
}
Trying a different approach. The FormField maintains it's own internal state and publishes (via completion) when its text is committed:
struct FormField : View {
#State private var output: String = ""
let viewModel: FormFieldViewModel
var didUpdateText: (String) -> ()
var body: some View {
VStack {
TextField($output, placeholder: Text(viewModel.placeholder), onCommit: {
self.didUpdateText(self.output)
})
Line(color: Color.lightGray)
}.padding()
}
}
ForEach(viewModel.viewModels) { vm in
FormField(viewModel: vm) { (output) in
vm.output = output
}
}
Swift 5.5
From Swift 5.5 version, you can use binding array directly by passing in the bindable like this.
ForEach($viewModel.viewModels, id: \.self) { $vm in
FormField(viewModel: $vm)
}
A solution could be the following:
ForEach(viewModel.viewModels.indices, id: \.self) { idx in
FormField(viewModel: self.$viewModel.viewModels[idx])
}
Took some time to figure out a solution to this puzzle. IMHO, it's a major omission, especially with SwiftUI Apps proposing documents that has models in struct and using Binding to detect changes.
It's not cute, and it takes a lot of CPU time, so I would not use this for large arrays, but this actually has the intended result, and, unless someone points out an error, it follows the intent of the ForEach limitation, which is to only reuse if the Identifiable element is identical.
ForEach(viewModel.viewModels) { vm in
ViewBuilder.buildBlock(viewModel.viewModels.firstIndex(of: zone) == nil
? ViewBuilder.buildEither(first: Spacer())
: ViewBuilder.buildEither(second: FormField(viewModel: $viewModel.viewModels[viewModel.viewModels.firstIndex(of: vm)!])))
}
For reference, the ViewBuilder.buildBlock idiom can be done in the root of the body element, but if you prefer, you can put this with an if.