SwiftUI - Popover in ForEach Loop - swiftui-foreach

hopefully anyone can help me with my problem in SwiftUI. I am displaying 30 Buttons in a ForEach loop and any of these Buttons should have their own popover.
My code is currently looking like this:
ForEach(0..<30, id: \.self) { index in
Button {
presentPopover = true
} label: {
ZStack {
Rectangle()
.fill(.white)
.frame(width: 180, height: 55)
.cornerRadius(5)
Text("Runde \(index + 1)")
.bold()
.font(.system(size: 24))
.foregroundColor(.black)
}
}
.popover(isPresented: $presentPopover) {
GameSheetPopOverView(points: $points)
}
}
How can i realize this with the $isPresented variable? Currently nothing happens when I tap on one of these Buttons. This only works if I have single elements without a ForEach Loop and a $isPresented variable for each of these elements.
I hope you can help.
Thanks in advance.

Put the stuff in the ForEach in its own View. The index and points should be parameters –

Related

SwiftUI: List, NavigationLink, and badges

I'm working on my first SwiftUI app, and in it would like to display a List of categories, with a badge indicating the number of items in that category. The title of the category would be on the left, and the badge would be right-aligned on the row. The list would consist of NavigationLinks so that tapping on one would drill further down into the view hierarchy. The code I've written to render the NavigationLinks looks like this:
List {
ForEach(myItems.indices) { categoryIndex in
let category = categories[categoryIndex]
let title = category.title
let fetchReq = FetchRequest<MyEntity>(entity: MyEntity(),
animation: .default)
NavigationLink(destination: MyItemView()) {
HStack(alignment: .center) {
Text(title)
Spacer()
ZStack {
Circle()
.foregroundColor(.gray)
Text("\(myItemsDict[category]?.count ?? 0)")
.foregroundColor(.white)
.font(Font.system(size: 12))
}
}
}
}
}
While it does render a functional NavigationLink, the badge is not displayed right-aligned, as I had hoped. Instead, it looks like this:
I know I'm getting hung up on something in my HStack, but am not sure what. How do I get it so that the category title Text takes up the majority of the row, with the badge right-aligned in the row?
SwiftUI doesn't know how big your Circle should be, so the Spacer doesn't do anything. You should set a fixed frame for it.
struct ContentView: View {
var body: some View {
List {
ForEach(0..<2) { categoryIndex in
let title = "Logins"
NavigationLink(destination: Text("Hi")) {
HStack(alignment: .center) {
Text(title)
Spacer()
ZStack {
Circle()
.foregroundColor(.gray)
.frame(width: 25, height: 25) // here!
Text("5")
.foregroundColor(.white)
.font(Font.system(size: 12))
}
}
}
}
}
}
}
Result:

Fixed View above a List View within a Navigation View

How do I get a fixed (not scrolling) view above a List view within a NavigationView in SwiftUI?
The first picture shows what I try to achive / have so far. However the second picture shos what happens if I pull down, the title slides above the fixed content.
Using offset on the List and a ZStack I got close but the NavigationView Title keeps scrolling above the fixed viewwhen pulling down on the list.
Although I thought this is a more general question, to not risk this question gets closed, here is the code I have:
NavigationView {
ZStack{
List {
/* .. */
}
.listStyle(PlainListStyle())
.offset(x: 0, y: showFilter ? 100 : 0)
if showFilter {
VStack {
HStack {
Button(action: {
}, label: {
Text("Button")
})
.padding(5)
.background(Color(red: 238/255, green: 238/255, blue: 239/255))
.cornerRadius(5)
.padding()
Spacer()
}
Spacer()
}
}
}
.navigationBarItems(trailing:
Button(action: {
// initial
self.showFilter.toggle()
// completely different, odd behaviour (navigation title never fades away)
// withAnimation{ self.showFilter.toggle() }
}) {
Image(systemName: "line.horizontal.3.decrease.circle")
}
)
.navigationBarTitle("List")
}
Offset is weak approach here, because user will not see last rows in list when filter is shown and there are items more than screen area.
You can consider variant with section as placeholder (it is sticky and not overlapped by header) for filter (maybe with some custom style for buttons), so simple demo based on your code snapshot is like below.
Tested with Xcode 12 / iOS 14
NavigationView {
List {
Section(header: Group {
if showFilter {
VStack {
HStack {
Button(action: {
}, label: {
Text("Button")
})
.padding(5)
.background(Color(red: 238/255, green: 238/255, blue: 239/255))
.cornerRadius(5)
.padding()
Spacer()
}
Spacer()
}
}
}) {
ForEach(0..<20) { i in
Text("Item \(i)")
}
}}
.listStyle(PlainListStyle())
Not sure I understood your problem, can you please share a gif/video of the problem in action?
Either way you can try putting the list and the fixed view inside the same ZStack, put the list before the fixed view and give the fixed view a position attribute.
If the ZStack is inside the NavigationView it shouldn't affect the fixed view.
The Sudo-code would, roughly, be like this -
NavigationView {
ZStack {
List ...
FixedView()
.position(x: 100, y: 100)
}
}
If this was not enough you can check out Paul Hudson's tutorial on Absolute Positioning here.
And for more help please post your code and give us a video/gif demonstration so we can understand your problem and test it beforehand :)

In SwiftUI, why will my buttons trigger if contained in a ZStack but not in an overlay on a VStack?

Thanks in advance for any advice you can give. I have a custom dropdown menu that I have built in SwiftUI:
struct PhoneTypeDropdown: View {
let phoneTypes:[PhoneType] = [.Cell, .Work, .Landline]
#State var isExpanded: Bool
var body : some View {
VStack (spacing: 0) {
HStack {
Text("select type").font(.footnote)
Spacer()
Image(systemName: Constants.Design.Image.IconString.ChevronDown)
.resizable()
.frame(width: Constants.Design.Measurement.DropDownIconWidth,
height: Constants.Design.Measurement.DropDownIconHeight)
}
.padding(Constants.Design.Measurement.PadMin)
.background(Constants.Design.Colors.LightGrey)
.cornerRadius(Constants.Design.Measurement.CornerRadius)
.onTapGesture {
self.isExpanded.toggle()
}
if isExpanded {
VStack (spacing: 0){
ForEach(0 ..< phoneTypes.count) { i in
Button(self.phoneTypes[i].description, action: {
print("button tapped")
self.isExpanded.toggle()
})
.buttonStyle(DropDownButtonStyle())
if i != self.phoneTypes.count - 1 {
Divider()
.padding(.vertical, 0)
.padding(.horizontal, Constants.Design.Measurement.Pad)
.foregroundColor(Constants.Design.Colors.DarkGrey)
}
}
}.background(Constants.Design.Colors.LightGrey)
.cornerRadius(Constants.Design.Measurement.CornerRadiusMin)
.padding(.top, Constants.Design.Measurement.PadMin)
}
}.frame(width: Constants.Design.Measurement.PhoneTypeFieldWidth)
.cornerRadius(Constants.Design.Measurement.CornerRadius)
}
}
When I went to utilize this in my view, I had planned on using it as an overlay like this:
VStack(spacing: 0) {
ProfileField(text: phone1,
label: Constants.Content.PrimaryPhone,
position: .Justified)
.overlay(
PhoneTypeDropdown(isExpanded: expandDropdown1)
.padding(.top, 32) //FIXME: Fix this hard-coded value.
.padding(.trailing, Constants.Design.Measurement.PadMax),
alignment: .topTrailing
)
}
However, although the above code will trigger on clicking and expand the dropdown box, tapping any of the Button objects inside the dropdown box does nothing. I then tried to implement the dropdown box using a ZStack like this:
ZStack(alignment: .topTrailing) {
ProfileField(text: phone1,
label: Constants.Content.PrimaryPhone,
position: .Justified)
PhoneTypeDropdown(isExpanded: expandDropdown1)
.padding(.top, 32) //FIXME: Fix this hard-coded value.
.padding(.trailing, Constants.Design.Measurement.PadMax)
}
The dropdown box worked beautifully, expanding and collapsing as expected. However, now, when it expands, it pushes down the rest of the form instead of laying on top of the form as desired.
My question then is this: what would cause my button action to fire correctly when using the dropdown object in a ZStack as opposed to incorporating it in an overlay on an view in a VStack?

Modal picker not scrolling right SwiftUI

I created a modal but it seems to have a bug on the selection. When scrolling the left, it scrolls the right, I have to go to the very edge of the left to be able to scroll, this is how it looks:
import SwiftUI
struct ContentView: View {
#State var showingModal = false
#State var hours: Int = 0
#State var minutes: Int = 0
var body: some View {
ZStack {
VStack {
Button("Show me"){
self.showingModal = true
}
if $showingModal.wrappedValue {
VStack(alignment: .center) {
ZStack{
Color.black.opacity(0.4)
.edgesIgnoringSafeArea(.vertical)
// this one is it
VStack(spacing: 20) {
Text("Time between meals")
.bold().padding()
.frame(maxWidth: .infinity)
.background(Color.yellow)
.foregroundColor(Color.white)
HStack {
Spacer()
VStack {
Picker("", selection: $hours){
ForEach(0..<4, id: \.self) { i in
Text("\(i) hours").tag(i)
}
}
.frame(width: 150, height: 120)
.clipped()
}
VStack {
Picker("", selection: $minutes){
ForEach(0..<60, id: \.self) { i in
Text("\(i) min").tag(i)
}
}
.frame(width: 150, height: 120)
.clipped()
}
}
Spacer()
Button(action: {
self.showingModal = false
}){
Text("Close")
} .padding()
}
.frame(width:300, height: 300)
.background(Color.white)
.cornerRadius(20).shadow(radius: 20)
}
}
}
}
}
}
}
How can I fix that little bug? I tried playing around with the layout but no use... any help would be appreciated
What if I told you the reason your Picker not working was this line?
.cornerRadius(20).shadow(radius: 20)
Unfortunately, SwiftUI is still quite buggy and sometimes it doesn't do what it is supposed to do and especially Pickers are not that reliable. I guess we'll need to wait and see the next iteration of SwiftUI, but for now you can replace that line with the code below:
.mask(RoundedRectangle(cornerRadius: 20))
.shadow(radius: 20)
There are just modifiers which affect all view hierarchy (ie. all subviews) that can change resulting layout/presentation/behaviour. And .cornerRadius and .shadow are such type modifiers.
The solution is to apply (as intended) those modifiers only to entire constructed view, and here it is
.compositingGroup() // <<< fix !!
.cornerRadius(20).shadow(radius: 20)
where .compositionGroup is intended to make above view hierarchy flat rendered and all below modifiers applied to only to that flat view.

List inside ScrollView is not displayed on WatchOs

I have a list inside a scrollview and it is not displaying below the image and the buttons. I've also tried to put the list and other items inside of a VStack and that allows me to see one item of the list at the time opposed to scrolling past the Image and buttons to show the whole list.
ScrollView{
Image(uiImage: self.image)
.resizable()
.frame(width: 80, height: 80)
.scaledToFit()
Text("\(name)")
.lineLimit(2)
HStack{
Button(action: {
print("button1")
}){
Image(systemName: "pencil")
}
Button(action: {
print("button 2")
}){
Image(systemName: "trash")
}
}
List{
ForEach(self.items, id: \.self) { item in
VStack{
Text(item.name)
.font(.headline)
.lineLimit(1)
Text(item.subname)
.font(.subheadline)
.lineLimit(1)
}
}
}
}
.navigationBarTitle(Text("Tittle"))
.edgesIgnoringSafeArea(.bottom)
Ive also tried to add .frame( minHeight: 0, maxHeight: .infinity)
to the list to force it to have its whole height and that did not work either. Any suggestions or might this be a swiftUI bug ?
EDIT
I just realized Im getting this error when scrolling:
APPNAME Watch Extension[336:60406] [detents] could not play detent NO, 2, Error Domain=NSOSStatusErrorDomain Code=-536870187 "(null)", (
{
Gain = "0.01799999922513962";
OutputType = 0;
SlotIndex = 4;
},
{
Gain = "0.6000000238418579";
OutputType = 1;
SlotIndex = 5;
}
)
Indicate some height for your List like
List{
ForEach(self.items, id: \.self) { item in
VStack{
Text(item.name)
.font(.headline)
.lineLimit(1)
Text(item.subname)
.font(.subheadline)
.lineLimit(1)
}
}
}.frame(minHeight: 200, maxHeight: .infinity)
Have you tried putting the List inside of a GeometryReader and setting the frame there?
I got the same error with UIKit. The error message is related to haptic feedback, see here.
It says that a haptic feedback could not be played, probably type 2, i.e. directionDown, see here.
Since my code does not call play(_:), it must be called by watchOS itself.
The reason for the error might be very fast scrolling, which could result in too frequent calls to play(_:) that cannot be handled properly. The docs say:
Do not call this method multiple times in quick succession. If the
haptic engine is already engaged when you call this method, the system
stops the current feedback and imposes a minimum delay of 100
milliseconds before engaging the engine to generate the new feedback.
If this is really an effect of watchOS, I guess you cannot do anything to avoid the error.