Add contextMenu in the items inside of a List in SwiftUI - contextmenu

I have created a photo library, there is list with many cells and in each cells. there are a few UIImages.
I added contextMenu the images, but when I long press on each image, the entire cell will be called instead of each image.
could anyone help me to how to add contextMenu to each the items inside of a List
struct PhotoList : View {
var photoLibrary = PhotoLibrary
var body : some View {
GeometryReader { geometry in
List(self.photoLibrary, id: \.self) { imageSet in
HStack (alignment: .center) {
ForEach(imageSet, id: \.self) { image in
Image(uiImage: image)
.scaledToFill()
.cornerRadius(7)
.padding(3)
.frame(width: 150, height: 150, alignment: .center)
.contextMenu {
VStack {
Button(action: {}) {
HStack {
Text("Add to Today")
Image("plus.circle")
}
}
}
}
}
}
}
}
}
}

The problem with this is that the list view will force touch events to be registered to the first child view. For example it is not possible to create a list with multiple buttons in each row as in the following code.
// Individual buttons can't be pressed
List(someList, id: \.self) { _ in
HStack {
ForEach(1..<4) { k in
Button("Button") {
//what ever
}
}
}
}
HStack will get all the touch events....
Though it is possible to solve your problem by using a combination of a scrollview and a vstack. If scrolling isn't necessary just remove that piece of code.
var body : some View {
GeometryReader() { geometry in
ScrollView {
VStack {
List(self.photoLibrary, id: \.self) { imageSet in
HStack (alignment: .center) {
ForEach(imageSet, id: \.self) { image in
Image(uiImage: image)
.resizable()
.scaledToFill()
.cornerRadius(7)
.padding(3)
.frame(width: 150, height: 150, alignment: .center)
.contextMenu {
VStack {
Button(action: {}) {
HStack {
Text("Add to Today")
Image("plus.circle")
}
}
}
}
}
}
}
}
}.position(x: geometry.size.width/2, y: geometry.size.height/2)
}
}
Code needs to be corrected for your design though. Added .resizable to the image as well. Hope this can be a start for your code.

Replace your List with a ForEach and wrap it in a VStack or LazyVStack and you're good to go. If you need scrolling wrap it in a ScrollView. The List makes it behave like table rows.
So instead of
List(self.photoLibrary, id: \.self) { imageSet in
// ...
}
do this:
ScrollView { // If scrolling is needed
VStack {
ForEach(self.photoLibrary, id: \.self) { imageSet in
// ...
}
}
}
Pretty much as NoosaMaan explained in more detail:
"[…] the list view will force touch events to be registered to the first child view […]"

I'm not sure what your struct for photoLibrary is like, so I created my own. This code will need to be changed slightly depending on your struct.
This ContentView can be called passing a list of photoLibrary to display on the screen.
struct photoLibrary {
var imageSet: [ImageItem]
init(){
self.imageSet = [ImageItem()]
}
}
struct ImageItem{
var image1: Image
var image2: Image
var image3: Image
init(){
self.image1 = Image(systemName: "xmark")
self.image2 = Image(systemName: "xmark")
self.image3 = Image(systemName: "xmark")
}
}
struct ContentView: View {
#State var photoLib: [photoLibrary]
var body: some View {
VStack {
ScrollView{
ForEach(self.photoLib.indices, id: \.self) { item in
VStack{
ForEach(photoLib[item].imageSet.indices, id: \.self) { image in
HStack{
ImageView(image: photoLib[item].imageSet[image].image1)
ImageView(image: photoLib[item].imageSet[image].image2)
ImageView(image: photoLib[item].imageSet[image].image3)
}
}
}
}
}
}
}
struct ImageView: View{
#State var image: Image
var body: some View{
image
.resizable()
.scaledToFill()
.cornerRadius(7)
.padding(3)
.frame(width: 150, height: 150, alignment: .center)
Text("Add to Today").onTapGesture {
//do something
}
Image(systemName: "plus.circle").onTapGesture {
//do something
}
}
}
}
Display example with xmarks as the image:

As I know a context menu has to have at least two buttons but the following way I think it would be enough:
Image(uiImage: image)
.scaledToFill()
.cornerRadius(7)
.padding(3)
.frame(width: 150, height: 150, alignment: .center)
.contextMenu {
Button(action: {}) {
Text("Add to Today")
Image("plus.circle")
}
Button(action: {}) {
Text("Some Other Button")
Image("globe")
}
}

Related

SwiftUI Custom Navigation Bar VStack doesn't work

I'm trying to make a custom navigation bar with back button, image, VStack (2 labels) but it didn't work. The whole view will stick to the center and not following the alignment I set. Thank you!
struct WeatherNavigation: View {
var body: some View {
HStack {
WeatherNavigation()
}
.
.
.
}
}
//
struct WeatherNavigation: View {
var body: some View {
Button(action: {
//action
}, label: {
HStack {
Image("Back")
.foregroundColor(.black)
Image("Weather")
.resizable()
.frame(width: 40, height: 40)
}
})
.frame(width: 100, height: 50, alignment: .leading)
VStack {
Text(weather.description)
.font(.appFont(size: 18))
.foregroundColor(Color(uiColor: .black))
Text(weather.location)
.font(.appFont(size: 12))
.foregroundColor(Color(uiColor: .blue))
}
.frame(width: .infinity, height: 50, alignment: .leading)
}
}
First of all, you shouldn't set frame of the whole view like that. For you problem, it can divide into: make a HStack to store all the container view and make a space between those two of them. Because using HStack you don't need to add leading.
Code will be like this
struct WeatherNavigation: View {
var body: some View {
// make a HStack to store all the attribute
HStack(alignment: .top) {
Button(action: {
//action
}, label: {
HStack {
Image("Back")
.foregroundColor(.black)
Image("Weather")
.resizable()
.frame(width: 40, height: 40)
}
})
.frame(width: 100, height: 50)
VStack {
Text("Tokyo")
.foregroundColor(Color(uiColor: .black))
Text("Japan")
.foregroundColor(Color(uiColor: .blue))
}
// make a space at the end
Spacer()
}
}
}
And the usage like this
struct ContentView: View {
var body: some View {
VStack {
HStack {
WeatherNavigation()
}
Spacer()
}
}
}
More over: adding Spacer() means that make space between view. That will solved your problem if you want to keep like your way.

Swift UI navigationLink from item

I have problem with making this itemView to navigationLink. I need onTapGesture to open next list
https://github.com/reddogwow/test/blob/main/MainMenu
var objectView: some View {
VStack {
Text(objectname)
.foregroundColor(.white)
.font(.system(size: 25, weight: .medium, design: .rounded))
Image(objectphoto)
.resizable()
.frame(width: 100, height: 100)
.clipShape(Circle())
.overlay(Circle().stroke(Color.white, lineWidth: 4))
}
.frame(height: 200)
.frame(maxWidth: .infinity)
.background(Color.blue)
}
Best edit will be where i can use Destination name from item (navMenu string)
I need something like this
var body: some View {
// NavigationView {
let columns = Array(
repeating: GridItem(.flexible(), spacing: spacing),
count: numbersOfColumns)
ScrollView {
HStack {
personView
petView
}
LazyVGrid(columns: columns, spacing: spacing) {
ForEach(items) { item in
NavigationLink(destination: item.navMenu) {
Text("")
} label: {
ItemView(item: item)
}
}
}
.padding(.horizontal)
}
.background(Color.blue.ignoresSafeArea())
.navigationTitle("")
// }
}
Where line NavigationLink(destination: HERE MUST BE STRING TO navMenu) But now im in cycle lot of fails
I have some menus called
Menu1.swift
Menu2.swift
Menu3.swift
I need open this menu after click on Grid menu.
But destination: Must be filled with name from item in code.
struct item: Identifiable {
let id = UUID()
let title: String
let image: String
let imgColor: Color
let navMenu : String
}
item(title: "Menu 1", image: "img1", imgColor: .orange, navMenu: "Menu1"),
I thing I have bad written buy maybe only small mistake
or maybe make it like this?
var navMenuDest = destination: + item.navMenu
this will be
NavigationLink(navMenuDest) {
in finale looks like
NavigationLink(destination: Menu1)
You must have a NavigationView in the hierarchy to use NavigationLink. To make each ItemView navigate to a new view when tapped, we use NavigationLink as shown below.
Code:
struct MainMenu: View {
/* ... */
var body: some View {
NavigationView {
let columns = Array(
repeating: GridItem(.flexible(), spacing: spacing),
count: numbersOfColumns)
ScrollView {
HStack {
personView
objectView
}
LazyVGrid(columns: columns, spacing: spacing) {
ForEach(items) { item in
NavigationLink {
Text("Some destination view here...\n\nItem: \(String(describing: item))")
} label: {
ItemView(item: item)
}
}
}
.padding(.horizontal)
}
.background(Color.blue.ignoresSafeArea())
.navigationTitle("Main Menu")
}
}
}
Result:

Implementing custom sidebar across all views - SwiftUI

OUTLINE
I have made a custom slimline sidebar that I am now implementing across the whole app. The sidebar consists of a main button that is always showing and when pressed it shows or hides the rest of the sidebar that consists of buttons navigating to other views.
I am currently implementing the sidebar across the app on each view by creating a ZStack like this:
struct MainView: View {
var body: some View {
ZStack(alignment: .topLeading) {
SideBarCustom()
Text("Hello, World!")
}
}
}
PROBLEM
I am planning on adding a GeometryReader so if the side bar is shown the rest of the content moves over. With this in mind, the way I am implementing the sidebar on every view feels clunky and a long winded way to add it. Is there a more simple/better method to add this to each view?
Sidebar Code:
struct SideBarCustom: View {
#State var isToggle = false
var names = ["Home", "Products", "Compare", "AR", "Search"]
var icons = ["house.fill", "printer.fill.and.paper.fill", "list.bullet.rectangle", "arkit", "magnifyingglass"]
var imgSize = 20
var body: some View {
GeometryReader { geo in
VStack {
Button(action: {
self.isToggle.toggle()
}, label: {
Image("hexagons")
.resizable()
.frame(width: 40, height: 40)
.padding(.bottom, 20)
})
if isToggle {
ZStack{
RoundedRectangle(cornerRadius: 5)
.foregroundColor(Color.red)
.frame(width: 70, height: geo.size.height)
VStack(alignment: .center, spacing: 60) {
ForEach(Array(zip(names, icons)), id: \.0) { item in
Button(action: {
// NAVIIGATE TO VIEW
}, label: {
VStack {
Image(systemName: item.1)
.resizable()
.frame(width: CGFloat(imgSize), height: CGFloat(imgSize))
Text(item.0)
}
})
}
}
}
}
}
}
}
}
I don't think there's necessarily a reason to use GeometryReader here. The following is an example that has a dynamic width sidebar (although you could set it to a fixed value) that slides in and out. The main content view resizes itself automatically, since it's in an HStack:
struct ContentView : View {
#State private var sidebarShown = false
var body: some View {
HStack {
if sidebarShown {
CustomSidebar(sidebarShown: $sidebarShown)
.frame(maxHeight: .infinity)
.border(Color.red)
.transition(sidebarShown ? .move(edge: .leading) : .move(edge: .trailing) )
}
ZStack(alignment: .topLeading) {
MainContentView()
.frame(maxWidth: .infinity, maxHeight: .infinity)
if !sidebarShown {
Button(action: {
withAnimation {
sidebarShown.toggle()
}
}) {
Image(systemName: "info.circle")
}
}
}
}
}
}
struct CustomSidebar : View {
#Binding var sidebarShown : Bool
var body: some View {
VStack {
Button(action: {
withAnimation {
sidebarShown.toggle()
}
}) {
Image(systemName: "info.circle")
}
Spacer()
Text("Hi")
Text("There")
Text("World")
Spacer()
}
}
}
struct MainContentView: View {
var body: some View {
VStack {
Text("Main content")
}
}
}

Buttons inside list item don't work properly

I have 2 views: PollCard and PollList (like list of polls)
In the PollCard view I have 2 buttons(images), that calls "answer" function:
HStack{
Button(action: {
self.answer()
print("Pressed first image")
}){
Image(poll.v1img)
.resizable()
.renderingMode(.original)
.scaledToFill()
.frame(width: 150, height: 200)
}.frame(width: 150, height: 200)
Button(action: { self.answer()}){
Image(poll.v2img )
.resizable()
.renderingMode(.original)
.frame(width: 150, height: 200)
}.frame(width: 150, height: 200).zIndex(4)
}
In the PollList view I have this simple list:
var body: some View {
HStack{
List(pollData) { poll in
PollCard(poll: poll)
}.padding()
}
}
But when I click the images in the list, it selects like all images and presses it
It is also very easy to check - terminal prints Pressed first image even if I've pressed only second image
What should I do to fix this?
As I mentioned in the comment section the workaround would be to substitute the HStack around the List with a ScrollView and the List with a ForEach:
struct ContentView: View {
struct Data: Identifiable {
var id: Int
}
#State var data = [Data(id: 0), Data(id: 1), Data(id: 2), Data(id: 3), Data(id: 4), Data(id: 5)]
var body: some View {
ScrollView {
ForEach(self.data) { data in
HStack {
Button(action: {
print("Pressed blue...")
}, label: {
Rectangle()
.foregroundColor(Color.blue)
.frame(width: 150, height: 200)
})
Button(action: {
print("Pressed red...")
}, label: {
Rectangle()
.foregroundColor(Color.red)
.frame(width: 150, height: 200)
})
}
}
}
}
}
I hope this helps!
this is an unexpected behavior of Button (or it is a bug?) in current SwiftUI (either on macOS or iOS).
The workaround in your case is simple, try to apply PlainButtonStyle for your buttons
import SwiftUI
struct ContentView: View {
var body: some View {
List {
Text("Hello, World!")
HStack {
Button(action: {
print("button1")
}) {
Color.yellow
}.buttonStyle(PlainButtonStyle())
Button(action: {
print("button2")
}) {
Color.green
}.buttonStyle(PlainButtonStyle())
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The funny thing is that it is enough to apply the style on one button only ... or apply it to parent HStack :-)
changing ContentView ...
struct ContentView: View {
var body: some View {
List {
Text("Hello, World!")
HStack {
Button(action: {
print("button1")
}) {
Color.yellow
}
Button(action: {
print("button2")
}) {
Color.green
}
}
.buttonStyle(PlainButtonStyle())
.frame(height: 100)
Text("By by, World!")
}
}
}
you get
where each of buttons works as expected

How to increase tappable area of navigationBarItem in SwiftUI?

I have this code for my view
struct ContentView: View {
var body: some View {
NavigationView{
List{
ForEach(0...5, id: \.self) { note in
VStack(alignment: .leading) {
Text("title")
Text("subtitle")
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
.navigationBarItems(trailing: resetButton)
.navigationBarTitle(Text("Notes"))
}
}
var resetButton: some View {
Button(action: {
print("reset")
}) {
Image(systemName: "arrow.clockwise")
}
.background(Color.yellow)
}
}
resetButton looks like this:
When I am tapping the resetButton, it seems like only the yellow area responds to touches.
How do I make tappable area of this button bigger? (Make it behave like a normal UIBarButtonItem)
You can change the frame of the view inside the button:
var resetButton: some View {
Button(action: {
print("reset")
}) {
Image(systemName: "arrow.clockwise")
.frame(width: 44, height: 44) // Or any other size you like
}
.background(Color.yellow)
}
This blog post pointed me in the right direction, I needed to add some padding directly to the Image.
Button(action: {
// Triggered code
}) {
Image(systemName: "plus")
.font(.system(size: 22, weight: .regular))
.padding(.vertical)
.padding(.leading, 60)
}
.background(Color.red) // Not needed

Resources