I have a ForEach loop which works with index, how I can make this ForEach step get custom step, because in ForEach in every single loop, it will add one to index, how we cam make step be 5, it means in every single loop adds 5 instead 1. I can make it possible with if and %, but I do not want ForEach try every each Item inside Items. how we can make it efficient coding?
Code:
struct ContentView: View
{
var body: some View
{
List
{
ForEach(0 ..< 21) { index in // (Step == 5) not 1
Text("Hello, world!")
.padding()
}
}
}
}
Here is possible approach
List
{
ForEach(Array(stride(from: 0, to: 21, by: 5)), id: \.self) { index in // (Step == 5) not 1
Text("Hello, world!")
.padding()
}
}
Related
I am developing a basic passcode entry screen, consisting off a top Stack to display currently entry, then some HStack's displaying the numbers
VStack(){
HStack(spacing: 20){
ForEach(codes,id: \.self){i in
Text("*")
}
}
HStack(){
<Number 1 - 3>
}
HStack(){
<Number 4 - 6>
}
HStack(){
<Number 7 - 9>
}
HStack(){
<Number 0>
}
}
This issue im facing is when there is no passcode entered the top HStack dosnt use up any space, so has a vertical height of 0, when I enter a code, it forces the whole view to jump a little as the view resizes.
How can I stop that so
If I'm being honest, it was quiet fun to build ! 😄 Don't forget to mark this answer as the right one if it solved your issue. ✅
PROBLEM
The jumping effect is due to SwiftUI updating all views positions based on available space calculated based on your content (passcode digits). The font, font weight, text size, etc… all has an effect on the available space left for other views.
SOLUTION
To avoid that, you need to a predefined frame that will let the parent view know that your digits will never take more space. Doing so, each update won't effect the position of any other view because the allocated top space would always be size you specified and not the digits sizes (or absence).
CODE
import SwiftUI
import Combine
// Using Combine to manage digits and future network calls…
class PasscodeManager: ObservableObject {
let codesQuantity = 4
#Published var codes = [Int]()
}
struct PasscodeView: View {
#StateObject private var manager = PasscodeManager()
var body: some View {
VStack {
Spacer()
// Dots placeholders and passcode digits
selectedCodes
Spacer()
// Numberpad
PasscodeLine(numbers: 1...3) { add(number: $0) }
PasscodeLine(numbers: 4...6) { add(number: $0) }
PasscodeLine(numbers: 7...9) { add(number: $0) }
PasscodeLine(numbers: 0...0) { add(number: $0) }
Spacer()
}
.padding()
}
var selectedCodes: some View {
let minDots = manager.codes.count == manager.codesQuantity ? 0:1
let maxDots = manager.codesQuantity - manager.codes.count
return HStack(spacing: 32) {
ForEach(manager.codes, id: \.self) { Text("\($0)") }
if maxDots != 0 {
ForEach(minDots...maxDots, id: \.self) { _ in
Circle().frame(width: 12)
}
}
}
.font(.title.bold())
// Setting a default height should fix your problem. 🙂
.frame(height: 70)
}
func add(number: Int) {
guard manager.codes.count < manager.codesQuantity else { return }
manager.codes.append(number)
}
}
struct PasscodeLine: View {
let numbers: ClosedRange<Int>
var select: (Int) -> Void
var body: some View {
HStack {
ForEach(numbers, id: \.self) { number in
Spacer()
Button(action: { select(number) },
label: {
Text("\(number)")
.font(.title)
.fontWeight(.medium)
.foregroundColor(Color(.label))
.padding(32)
.background(Color(.quaternarySystemFill))
.clipShape(Circle())
})
}
Spacer()
}
}
}
RESULT
So I'm iterating over items in an array, I wish to display an ad say after every 3 or if the current index is divisble/a multiple of 3.
if !homeViewModel.posts.isEmpty {
ForEach(homeViewModel.posts, id: \.postID) { post in
HeaderCell(post: post)
GADNativeViewControllerWrapper()
.frame(height: 250)
.padding(10)
}
Yes, I am aware I will be showing an ad after every cell but I have tried iterating over the .indicies which has more of less given me the same result. How do I get the curreny index of post in and check it's position in the array?
I'm just lost here right now, any help would be appreciated.
Thank you.
To display an ad every 3rd element in an array you need to iterate over the .indices and conditionally display content based on the current index.
struct ContentView: View {
let posts = [1,2,3,4, 5, 6, 7, 8, 9, 10]
var body: some View {
VStack {
ForEach(posts.indices, id: \.self) { index in
HStack {
Text("Index: \(index)").bold()
Text("Post: \(posts[index])")
}
if (index + 1) % 3 == 0 {
Rectangle().fill(Color.blue)
.frame(height: 30)
}
}
}
}
}
Result:
In learning SwiftUI I'm trying to do transition animations I used with UIKit. I sometimes used pages with tables that changed with a parameter by using a containerView and transitioning between children. The SwiftUI equivalent seems to be a single List View dependent on the parameter. But it seems that .transitions don't work well since no view is being removed/inserted in the hierarchy (and simple animations don't give me the transition options I want). The only way I have made it work at all is by "manually" removing the old List and inserting the new one, but that seems a kludge and didn't work perfectly. Here's a toy version of the code that illustrated the problem -- the inserted green rectangle is just for comparison -- the transition works fine for it but not for the List.
struct ContentView: View {
let listItems:[[String]] = [["A0","B0"],["A1","B1"]]
#State var pointer:Int = 0
var body: some View {
VStack{
List(listItems[pointer], id:\.self)
{item in Text(item)
.foregroundColor(self.pointer == 0 ? Color.blue : Color.purple)
}
.transition(.offset(x: -600, y: 0))
if pointer == 1 {
RoundedRectangle(cornerRadius: 10)
.frame(width: 300, height: 200)
.foregroundColor(.green)
.transition(.offset(x: 500, y: 300))
}
}
.onTapGesture {
withAnimation(.easeInOut(duration: 1)){
self.pointer = (self.pointer + 1) % 2
}
}
}
}
I've found a partial solution, but it behaves strangely. Instead of a single List with varying content, transitions seem to require separate lists which are inserted/removed. Here is a simple implementation
struct ContentView: View {
let listItems:[[String]] = [["A0","B0"],["A1","B1"],["A2","B2"]]
#State var pointer:Int = 0
var body: some View {
ZStack{ForEach(0...2, id: \.self)
{index in
ZStack{
if index == self.pointer {
List(self.listItems[index], id:\.self)
{item in HStack{Text(item);Spacer()}
.foregroundColor(self.pointer == 0 ? Color.blue : Color.purple)
.frame(height:20)
.padding(2)
.background(Color.yellow)
}
.transition(.asymmetric(insertion: .offset(x: 400, y: -370), removal: .offset(x: 0, y: -870)))
}
}
}
}
.onTapGesture {
withAnimation(.easeInOut(duration: 1)){
self.pointer = (self.pointer + 1) % 3
}
}
But it responds oddly to choice of offsets -- I don't understand the -370 y-offset needed to make insertions come in horizontally -- it was found by trial/error. And the transition 2->0 is different from the other two.
I'm new to Swift development, and I'm trying to make a View, where you can click an item and it gets bigger, while the old big item gets smaller. I'm using an #State var called chosen to know which Element should be big at the moment. The items itself are Views with a Button on top. The idea is, that I click the button and the button will change the chosen variable, which is working. But it seems that my view doesn't redraw itself and everything stays as is. The simplified pseudocode looks like this:
struct MyView: View {
#State var chosen = 0
var body: some View {
VStack(){
ForEach(0 ..< 4) { number in
if self.chosen == number {
DifferentView()
.frame(big)
.clipShape(big)
}else{
ZStack{
DifferentView()
.frame(small)
.clipShape(small)
Button(action: {self.chosen = number}){Rectangle()}
}
}
}
}
}
You're using this overload of ForEach.init(_:content:), which accepts a constant range. While your range doesn't change, it also appears to be that this ForEach variant doesn't update the content (it was surprising to me).
You need to use the following overload: ForEach.init(_:id:content:) - supplying id with a keypath:
ForEach(0 ..< 4, id: \.self) { number in
// ...
}
But because there is a conditional, it trips up SwiftUI (hard to know why). The way to avoid it is to wrap it in something, like a Group or a ZStack, or even a function that generates the inner view:
ForEach(0 ..< 4, id: \.self) { number in
Group {
self.chosen == number {
// ...
} else {
// ...
}
}
}
Or, like so:
ForEach(0 ..< 4, id: \.self) { number in
self.inner(for: number)
}
#ViewBuilder
func inner(for number: Int) -> some View {
self.chosen == number {
// ...
} else {
// ...
}
}
I have such VStack with list inside it
VStack(alignment: .leading, spacing: 16) {
Text("Contacts")
.font(.custom("AvenirNext-DemiBold", size: 20))
.foregroundColor(Color("DarkTitle"))
.padding(8).layoutPriority(1)
List(self.contacts) { contact in
ContactOption(contact: contact)
.padding(.horizontal, 4)
} //.frame(height: 240)
}
The problem with this code is that List tries to expand content as much as it can here taking up entire screen in spite of having just 4 contacts.
I can set this height to fixed value using frame(height: 240)
I consider wether there is possibility to enforce List to wrap its content like Text() view does.
i.e. if there is 4 rows in List wrap content to display just this 4 rows, if there is 8 rows expand to this 8 rows. Then I could set some max height ex. 400 above which List could not expand anymore and then it will be scrollable.
ok, i tried a bit and i am not sure whether you can use it or not, but check this out: (just tap on add and remofe to see how the list gets bigger and smaller)
struct ContactOption : View {
var contact: String
var body: some View {
Text(contact)
}
}
struct ListView : View {
var contacts: [String]
var body : some View {
// List(self.contacts, id: \.self) { contact in
// ContactOption(contact: contact)
// .padding(.horizontal, 4)
// }
List {
ForEach (contacts, id: \.self) { contact in
Text (contact)
}
}
}
}
struct ContentView: View {
#State var contacts = ["Chris", "Joe", "Carla", "another"]
var body: some View {
VStack() {
HStack {
Button("Add") {
self.contacts.append("dust")
}
Button("Remove") {
self.contacts = self.contacts.dropLast()
}
}
Text("Contacts")
.foregroundColor(Color.blue)
.padding(8).layoutPriority(1)
Form {
ListView(contacts: contacts)
Section(footer: Text("hi")) {
Text("hi")
}
}
Divider()
Text("end list")
.foregroundColor(Color.orange)
}
}
}