How to get continuous index numbers from nested ForEach loop? - swiftui

I'm using a 4-block grid for my Widget Extension.
I want to populate these grids with an array of elements.
let exampleArray = ["test1","test2","test3","test4"]
When I use two nested ForEach loops, I can't get the element in the 4th index.
VStack {
ForEach((0..<2)) { column in
HStack {
ForEach((0..<2)) { index in
ZStack (alignment: .bottomLeading) {
Text(newsList[(index+column)]) // 0+0, 1+0, 0+1, 1+1
// Text = test1, test2, test2, test3
}
}
}
}
}
Any way I can work around this? Because it's in View, I can't use any operations.
Full code of the View:
struct AppWidgetEntryView : View {
var entry: Provider.Entry
#Environment(\.widgetFamily)
var widgetFamily
var body: some View {
VStack(alignment: .trailing, spacing: 6) {
Image("logo")
.frame(maxWidth: .infinity, alignment: .leading)
if widgetFamily == .systemLarge {
VStack {
ForEach((0..<2)) { column in
HStack {
ForEach((0..<2)) { index in
ZStack (alignment: .bottomLeading) {
if let url = URL(string: imageList[(index+column)]), let imageData = try? Data(contentsOf: url),
let uiImage = UIImage(data: imageData) {
Image(uiImage: uiImage)
.centerCropped()
.frame(maxHeight: 150, alignment: .center)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray, lineWidth: 1))
.shadow(radius: 10)
} else {
Image("ph_background")
.centerCropped()
.frame(maxHeight: 150, alignment: .center)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray, lineWidth: 1))
.shadow(radius: 10)
}
Text(newsList[(index+column)])
.font(.system(size: 12))
.foregroundColor(.white)
.fontWeight(.light)
// .frame(maxHeight: 50)
.background(Rectangle().fill(Color.black).blur(radius: 20))
.padding(.bottom, 5)
.padding(.leading, 5)
.padding(.trailing, 5)
.padding(.top, 5)
}
}
}
}
}
}
}
}
Solution
Please check the accepted answer.
struct AppWidgetEntryView : View {
var entry: Provider.Entry
#Environment(\.widgetFamily)
var widgetFamily
var body: some View {
var columns: [GridItem] =
Array(repeating: .init(.fixed(100)), count: 2)
if widgetFamily == .systemLarge {
LazyVGrid(columns: columns) {
ForEach((0..<4)) { index in
ZStack (alignment: .bottomLeading) {
if let url = URL(string: imageList[(index)]), let imageData = try? Data(contentsOf: url),
let uiImage = UIImage(data: imageData) {
Image(uiImage: uiImage)
.centerCropped()
.frame(maxHeight: 150, alignment: .center)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray, lineWidth: 1))
.shadow(radius: 10)
} else {
Image("ph_background")
.centerCropped()
.frame(maxHeight: 150, alignment: .center)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray, lineWidth: 1))
.shadow(radius: 10)
}
Text(newsList[(index)])
.font(.system(size: 12))
.foregroundColor(.white)
.fontWeight(.light)
// .frame(maxHeight: 50)
.background(Rectangle().fill(Color.black).blur(radius: 20))
.padding(.bottom, 5)
.padding(.leading, 5)
.padding(.trailing, 5)
.padding(.top, 5)
}
}
}
}
}
}

Use LazyVGrid
I feel like you really should be using LazyVGrid for this...
https://developer.apple.com/documentation/swiftui/lazyvgrid
It works like a List but it places each item in a grid of columns.
Using this will solve this problem for you.
Without LazyVGrid
If you really don't want to use a LazyVGrid (please use this though). Then the calculation you are using is not correct. The calculation is something like...
index = row*numberOfCols + column
You can't just use the row as that will only increase the index by 1 per row. But you need to increase the index by the number of columns per row (2 in your case).

Related

Swift ScrollView goes on top again

I have a simple screen in which content are going on bottom so I use ScrollView on it issue is when I release scroll its going to top again. I need to replace scrollview to something Because I try with with List but that's not working properly with background image so I use Scrollview
struct signupView: View {
#StateObject var signUpViewModel = signupViewModel()
var body: some View {
ZStack{
GeometryReader { geo in
ScrollView {
VStack{
VStack{
Image("Untitled-2")
.resizable()
.frame(width: geo.size.width * 0.45, height: geo.size.width * 0.45)
}
.padding(.top, geo.size.height * 0.03)
HStack(spacing: 0){
VStack{
Text("Student")
.foregroundColor(signUpViewModel.isStudent ? Color("secondarycolor") : Color("authColorText"))
Rectangle()
.fill(signUpViewModel.isStudent ? Color("secondarycolor") : Color("authColorText"))
.frame(width: geo.size.width * 0.4, height: 2)
}
.onTapGesture {
signUpViewModel.isStudent = true
}
VStack{
Text("Facilitator")
.foregroundColor(!signUpViewModel.isStudent ? Color("secondarycolor") : Color("authColorText"))
Rectangle()
.fill(!signUpViewModel.isStudent ? Color("secondarycolor") : Color("authColorText"))
.frame(width: geo.size.width * 0.4, height: 2)
}
.onTapGesture {
signUpViewModel.isStudent = false
}
}
VStack{
VStack{
Spacer().frame(height: geo.size.height * 0.7)
TextField("", text: $signUpViewModel.nameField)
.modifier(PlaceholderStyle(showPlaceHolder: signUpViewModel.nameField.isEmpty,
placeholder: "Name"))
.padding()
.background(RoundedRectangle(cornerRadius: 50).fill(Color("primarycolorwithopacity")))
.foregroundColor(Color("authColorText"))
.foregroundColor(Color("authColorText")).font(Font.headline.weight(.medium))
.padding(.bottom, 10)
TextField("", text: $signUpViewModel.emailField)
.modifier(PlaceholderStyle(showPlaceHolder: signUpViewModel.emailField.isEmpty,
placeholder: "Email"))
.padding()
.background(RoundedRectangle(cornerRadius: 50).fill(Color("primarycolorwithopacity")))
.foregroundColor(Color("authColorText"))
.foregroundColor(Color("authColorText")).font(Font.headline.weight(.medium))
.padding(.bottom, 10)
SecureField("", text: $signUpViewModel.passwordField)
.modifier(PlaceholderStyle(showPlaceHolder: signUpViewModel.passwordField.isEmpty,
placeholder: "Password"))
.padding()
.background(RoundedRectangle(cornerRadius: 50).fill(Color("primarycolorwithopacity")))
.foregroundColor(Color("authColorText"))
.foregroundColor(Color("authColorText")).font(Font.headline.weight(.medium))
.padding(.bottom, 10)
SecureField("", text: $signUpViewModel.passwordRepeatField)
.modifier(PlaceholderStyle(showPlaceHolder: signUpViewModel.passwordRepeatField.isEmpty,
placeholder: "Repeat Password"))
.padding()
.background(RoundedRectangle(cornerRadius: 50).fill(Color("primarycolorwithopacity")))
.foregroundColor(Color("authColorText"))
.foregroundColor(Color("authColorText")).font(Font.headline.weight(.medium))
.padding(.bottom, 10)
TextField("", text: $signUpViewModel.countryField)
.modifier(PlaceholderStyle(showPlaceHolder: signUpViewModel.countryField.isEmpty,
placeholder: "Select Country"))
.padding()
.background(RoundedRectangle(cornerRadius: 50).fill(Color.black))
.foregroundColor(Color("authColorText"))
.foregroundColor(Color("authColorText")).font(Font.headline.weight(.medium))
.padding(.bottom, 10)
.overlay(
ZStack{if signUpViewModel.hasOptions {
Spacer().frame(height: 70)
VStack(alignment: .center) {
ForEach(signUpViewModel.countryoptions, id: \.self) {d in
Text(d).frame(maxWidth: .infinity).padding(8).onTapGesture {
signUpViewModel.countryField = d
signUpViewModel.hasOptions = false
}
Divider()
}
}.frame(maxWidth: .infinity).background(RoundedRectangle(cornerRadius: 6).foregroundColor(.white).shadow(radius: 4))
}
}.offset(y: 50)
, alignment: .topLeading)
.overlay(
Image("Untitled-42")
.resizable()
.foregroundColor(.white)
.frame(width: 15, height: 10).padding().padding(.bottom,2).onTapGesture {
signUpViewModel.hasOptions = !signUpViewModel.hasOptions
}
, alignment: .trailing).zIndex(1)
SecureField("", text: $signUpViewModel.passwordRepeatField)
.modifier(PlaceholderStyle(showPlaceHolder: signUpViewModel.passwordRepeatField.isEmpty,
placeholder: "Repeat Password"))
.padding()
.background(RoundedRectangle(cornerRadius: 50).fill(Color("primarycolorwithopacity")))
.foregroundColor(Color("authColorText"))
.foregroundColor(Color("authColorText")).font(Font.headline.weight(.medium))
.padding(.bottom, 10)
}.padding([.top, .leading, .trailing], 8).frame(height: 45)
}
}
.padding()
}
}
}.background(Image("Untitled-7").resizable()).edgesIgnoringSafeArea(.all)
}
}
When I release its going back to top. Don't get it why its happening on this. Or I need to use something else
One way of doing it is with introspect package, you can add the package here https://github.com/siteline/SwiftUI-Introspect.git
scrollView.bounces = false
Here's how I would do it with Introspect:
struct ContentView: View {
var body: some View {
ScrollView {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("Hello, world!")
}
.padding()
}.introspectScrollView { scrollView in
scrollView.bounces = false
}
}
}
also don't forget the imports
import Introspect

HStack in VStack How can I force background color + get rid of dividing lines?

I don't want the behavior I'm getting with this SwiftUI thing (first time messing with it). I've been putting .background() on everything and there's some kind of padding happening and some sort of dividing line, whether I enable the Button code or not (pic below is with Button code commented out).
What do I need to do to fix it?
var body: some View {
ZStack() {
Color.black
.ignoresSafeArea(.all)
VStack(alignment: .leading, spacing: 0) {
List(eventFields) { eventField in
HStack() {
Spacer(minLength: 10)
if let iconName = eventField.iconName {
Button(action: {
print("edit \(eventField.name)")
}, label: {
Image(uiImage: UIImage(named: iconName)!.colorizeMask(eventField.iconColor!))
.frame(width: 27, height: 27)
self.background(.black)
}).background(.black)
} else {
Text("")
.frame(width: 27)
}
Text(eventField.iconName == nil ? "" : eventField.name)
.font(.system(size: eventField.labelFontSize))
.foregroundColor(eventField.labelFontColor)
.frame(width: 50, alignment: .trailing)
Spacer(minLength: 3)
Text(eventField.stringValue)
.font(.system(size: eventField.fontSize))
.foregroundColor(eventField.fontColor)
.frame(width: 200, alignment: .leading)
Spacer(minLength: 10)
}.background(.black)
}.background(.black)
}
}
}
}
instead of putting .background on the HStack, use
.listRowBackground(Color.black)
and for separator use
.listRowSeparator(.hidden)
Keep in mind, this is on the HStack not the List
Full Code:
var body: some View {
ZStack() {
Color.black
.ignoresSafeArea(.all)
VStack(alignment: .leading, spacing: 0) {
List(eventFields) { eventField in
HStack() {
Spacer(minLength: 10)
if let iconName = eventField.iconName {
Button(action: {
print("edit \(eventField.name)")
}, label: {
Image(uiImage: UIImage(named: iconName)!.colorizeMask(eventField.iconColor!))
.frame(width: 27, height: 27)
self.background(.black)
}).background(.black)
} else {
Text("")
.frame(width: 27)
}
Text(eventField.iconName == nil ? "" : eventField.name)
.font(.system(size: eventField.labelFontSize))
.foregroundColor(eventField.labelFontColor)
.frame(width: 50, alignment: .trailing)
Spacer(minLength: 3)
Text(eventField.stringValue)
.font(.system(size: eventField.fontSize))
.foregroundColor(eventField.fontColor)
.frame(width: 200, alignment: .leading)
Spacer(minLength: 10)
}
.listRowBackground(Color.black)
.listRowSeparator(.hidden)
}
}
}
}
}
I believe the color specification for your hstack and frames is supposed to be "(Color.black)" instead of just "(.black)".
Which type of color you use isn't consistent across all Swift objects. Some objects, such as UITableView use "UI colors" which are in the form ".black", while others, like frames, vstacks, hstacks and other objects, use SwiftUI colors in the form "Color.black".
I recommend this very informative page for a very accessible explanation of using color in a view and a stack.

How to set a Limit for the the views inside a HStack?

Good day, beginner SwiftUI & iOS developer here.
I'm not quite sure how else I could've worded this question, but I'll try my best to explain what I would like to achieve.
Right now, I have a VStack that contains a WebImage and Text view, and this VStack is nested inside a HStack. The views inside the VStack are inside a ForEach loop and are generated dynamically with the data I fetch.
When I display these on a screen, all of these views appear in a single line, as shown below.
However I would like for there to only be max two views per "line", not all four of them stacked into a single line. Is there a way to achieve this?
Here is the code:
HStack(spacing: 20) {
ForEach(attViewModel.students, id: \.self) { student in
VStack {
WebImage(url: URL(string: student.photo))
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 40, height: 40)
.clipShape(Circle())
.overlay(Circle().stroke(Color("DarkGreen"), lineWidth: 3))
.compositingGroup()
Text("\(student.name)")
.bold()
.compositingGroup()
CustomRadioButton()
}
.padding()
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(Color.orange, lineWidth: 2))
.shadow(radius: 7)
}
}
.frame(maxWidth: .infinity)
Here a possible approach for you:
struct ContentView: View {
let arrayOfStudents: [String] = ["jessy", "joy", "joly", "jack"]
var body: some View {
GeometryReader { proxy in
ScrollView(.horizontal) {
HStack(spacing: .zero) {
ForEach(arrayOfStudents, id: \.self) { student in
VStack {
Image(systemName: "person")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 40, height: 40)
.padding()
.clipShape(Circle())
.overlay(Circle().stroke(Color.green, lineWidth: 3))
Text(student)
.bold()
Circle()
.strokeBorder(style: .init(lineWidth: 2))
.frame(width: 10, height: 10)
}
.compositingGroup()
.padding()
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.orange, lineWidth: 2))
.shadow(radius: 7)
.padding()
.frame(width: proxy.size.width/2.0)
}
}
}
.position(x: proxy.size.width/2.0, y: proxy.size.height/2.0)
}
}
}

How to show multiple entry content in a timeline?

I am creating a widget for my application.
For the large size of the widget, I want to show 4 cells with different content that I fetch from an API call. But I couldn't manage to do it. This is how my widget looks like currently:
Should I create a different entries struct for every cell? Or is there a way to put array/list in the entry struct and append 4 data I got from the request?
Any suggestions about how can I achieve this?
My entry struct:
struct NewsEntry: TimelineEntry {
var date: Date
let configuration: ConfigurationIntent
let header: String
let imageUrl: String
}
My getTimeline function:
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: #escaping (Timeline<Entry>) -> ()) {
var entries: [NewsEntry] = []
let currentDate = Date()
let later = Calendar.current.date(byAdding: .minute, value: 5, to: Date())!
NewsProvider.request() { (data, error) in
guard let data = data else {
if let error = error {
print(error)
}
return
}
let entry = NewsEntry(date: currentDate, configuration: configuration, header: data[0].title!, imageUrl: data[0].imageUrlDetail!)
entries.append(entry)
let timeline = Timeline(entries: entries, policy: .after(later))
completion(timeline)
}
}
My EntryView:
struct WidgetEntryView : View {
var entry: Provider.Entry
#Environment(\.widgetFamily)
var widgetFamily
var body: some View {
VStack(alignment: .trailing, spacing: 6) {
Image("logo")
.frame(maxWidth: .infinity, alignment: .leading)
if widgetFamily == .systemLarge {
var columns: [GridItem] =
Array(repeating: .init(), count: 2)
LazyVGrid(columns: columns) {
ForEach((0..<4)) { index in
ZStack (alignment: .bottomLeading) {
if let url = URL(string: entry.imageUrl), let imageData = try? Data(contentsOf: url),
let uiImage = UIImage(data: imageData) {
Image(uiImage: uiImage)
.centerCropped()
.frame(maxHeight: 150, alignment: .center)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray, lineWidth: 1))
.shadow(radius: 10)
} else {
Image("ph_background")
.centerCropped()
.frame(maxHeight: 150, alignment: .center)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray, lineWidth: 1))
.shadow(radius: 10)
}
Text(entry.header)
.font(.system(size: 12))
.foregroundColor(.white)
.fontWeight(.light)
// .frame(maxHeight: 50)
.background(Rectangle().fill(Color.black).blur(radius: 20))
.padding(.bottom, 5)
.padding(.leading, 5)
.padding(.trailing, 5)
.padding(.top, 5)
}
}
.frame(height: 160)
}
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.padding()
.background(LinearGradient(gradient: Gradient(colors: [Color(red:0.2, green:0.2, blue:0.2), .black]), startPoint: .top, endPoint: .bottom))
}
}
Changed the variables to [String] in the TimelineEntry and I achieved what I want.
I updated my struct and entry line like this:
var newsList: [String] = [] // First, appending the fetched data here
var imageList: [String] = []
struct NewsEntry: TimelineEntry {
var date: Date
let configuration: ConfigurationIntent
let header: [String]
let imageUrl: [String]
}
for index in 0 ..< 5 {
newsList.append(data[index].title!)
imageList.append(data[index].imageUrlDetail!)
}
// Then using passing the arrays to TimelineEntry as [String]
for hourOffset in 0 ..< 5 {
let entryDate = Calendar.current.date(byAdding: .minute, value: hourOffset, to: currentDate)!
let entry = NewsEntry(date: entryDate, configuration: configuration, header: newsList, imageUrl: imageList)
entries.append(entry)
}
The only problem right now is I don't know if I'm using it correctly because I get crash with "Index out of range" on entry.imageUrl[index]. This is the LazyVGrid my updated View function:
LazyVGrid(columns: columns) {
ForEach((0..<4)) { index in
ZStack (alignment: .bottomLeading) {
if let url = URL(string: entry.imageUrl[index]), let imageData = try? Data(contentsOf: url),
let uiImage = UIImage(data: imageData) { // Thread 1: Fatal error: Index out of range
Image(uiImage: uiImage)
.centerCropped()
.frame(maxHeight: 150, alignment: .center)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray, lineWidth: 1))
.shadow(radius: 10)
} else {
Image("ph_background")
.centerCropped()
.frame(maxHeight: 150, alignment: .center)
.cornerRadius(10)
.overlay(RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray, lineWidth: 1))
.shadow(radius: 10)
}
Text(entry.header[index])
.font(.system(size: 12))
.foregroundColor(.white)
.fontWeight(.light)
// .frame(maxHeight: 50)
.background(Rectangle().fill(Color.black).blur(radius: 20))
.padding(.bottom, 5)
.padding(.leading, 5)
.padding(.trailing, 5)
.padding(.top, 5)
}
}
.frame(height: 160)
}

How to ensure view appears above other views when iterating with ForEach in SwiftUI?

I have a SwiftUI view that is a circular view which when tapped opens up and is supposed to extend over the UI to its right. How can I make sure that it will appear atop the other ui? The other UI elements were created using a ForEach loop. I tried zindex but it doesn't do the trick. What am I missing?
ZStack {
VStack(alignment: .leading) {
Text("ALL WORKSTATIONS")
ZStack {
ChartBackground()
HStack(alignment: .bottom, spacing: 15.0) {
ForEach(Array(zip(1..., dataPoints)), id: \.1.id) { number, point in
VStack(alignment: .center, spacing: 5) {
DataCircle().zIndex(10)
ChartBar(percentage: point.percentage).zIndex(-1)
Text(point.month)
.font(.caption)
}
.frame(width: 25.0, height: 200.0, alignment: .bottom)
.animation(.default)
}
}
.offset(x: 30, y: 20)
}
.frame(width: 500, height: 300, alignment: .center)
}
}
}
}
.zIndex have effect for views within one container. So to solve your case, as I assume expanded DataCircle on click, you need to increase zIndex of entire bar VStack per that click by introducing some kind of handling selection.
Here is simplified replicated demo to show the effect
struct TestBarZIndex: View {
#State private var selection: Int? = nil
var body: some View {
ZStack {
VStack(alignment: .leading) {
Text("ALL WORKSTATIONS")
ZStack {
Rectangle().fill(Color.yellow)//ChartBackground()
HStack(alignment: .bottom, spacing: 15.0) {
ForEach(1...10) { number in
VStack(spacing: 5) {
Spacer()
ZStack() { // DataCircle()
Circle().fill(Color.pink).frame(width: 20, height: 20)
.onTapGesture { self.selection = number }
if number == self.selection {
Text("Top Description").fixedSize()
}
}
Rectangle().fill(Color.green) // ChartBar()
.frame(width: 20, height: CGFloat(Int.random(in: 40...150)))
Text("Jun")
.font(.caption)
}.zIndex(number == self.selection ? 1 : 0) // << here !!
.frame(width: 25.0, height: 200.0, alignment: .bottom)
.animation(.default)
}
}
}
.frame(height: 300)
}
}
}
}