List selection not changing value - ios

I want to change a #State var variable whenever a row in a list is being selected. Similar to what didSelectRow does in UIKit. That means when tapping on "One" the text on the right-hand side should change from "Nothing selected" to "One".
Here's my current implementation. However, tapping on a row does nothing at all.
struct ContentView: View {
#State var items = [Item(id: 1, text: "One"), Item(id: 2, text: "Two"), Item(id: 3, text: "Three")]
#State var selectedItem: Item? = nil
var body: some View {
GeometryReader { geometry in
HStack(alignment: .center) {
List(items, selection: $selectedItem) { item in
Text(item.text)
}
.frame(width: geometry.size.width / 2.5, height: geometry.size.height)
.listStyle(InsetGroupedListStyle())
Text(selectedItem?.text ?? "Nothing selected")
}
}
}
}
struct Item: Identifiable, Hashable {
var id: Int
var text: String
}
How can I change the text when someone taps a row?

Selection in List works only if EditMode is in active state, so you need to handle it manually, something like
var body: some View {
GeometryReader { geometry in
HStack(alignment: .center) {
List(items) { item in
Text(item.text)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
.contentShape(Rectangle())
.onTapGesture {
selectedItem = item
}
}
.frame(width: geometry.size.width / 2.5, height: geometry.size.height)
.listStyle(InsetGroupedListStyle())
Text(selectedItem?.text ?? "Nothing selected")
}
}
}
and if needed to highlight background than also do it manually.

You can use Button inside your List, and then select the Item in their action like this
List(items, id:\.self, selection: $selectedItem) { item in
Button(action: {
self.selectedItem = item
})
{
Text(item.text)
}
}

Related

SwiftUI Rotation Animation off center

I am trying to make a simple dropdown list item in SwiftUI. This is what the code looks like:
struct SomeObject: Hashable {
var title: String = "title"
var entries: [String] = ["details", "details2", "details3"]
}
struct ContentView: View {
var data: [SomeObject] = [SomeObject()]
var body: some View {
List(data, id: \.self) { item in
HStack {
Text(item.title)
Spacer()
}
ForEach(item.entries, id: \.self) { entry in
ListItemView(entry)
}
}.listStyle(.plain)
}
}
struct ListItemView: View {
#State var expanded: Bool = false
#State var rotation: Double = 0
private let entry: String
init(_ entry: String) {
self.entry = entry
}
var body: some View {
VStack {
Divider().frame(maxWidth: .infinity)
.overlay(.black)
HStack {
Text(entry)
.fixedSize(horizontal: false, vertical: true)
Spacer()
Image(systemName: "chevron.down")
.foregroundColor(.black)
.padding()
.rotationEffect(.degrees(expanded ? 180 : 360))
.animation(.linear(duration: 0.3), value: expanded)
}.padding(.horizontal)
.padding(.vertical, 6)
if expanded {
Text("Details")
}
Divider().frame(maxWidth: .infinity)
.overlay(.black)
}
.listRowSeparator(.hidden)
.listRowInsets(.init())
.onTapGesture {
expanded.toggle()
}
}
}
For some reason when clicking on the list item the animation looks like this:
How can I make the arrow rotate on its center point without moving up or down at all?
The problem you have there is that the arrow is animated but when the hidden text appears, that vertical expansion is not animated. That contrast between an element animated and another that is not makes the chevron looks like it is not doing it properly. So, try to animate the VStack like this:
struct CombineView: View {
#State var expanded: Bool = false
#State var rotation: Double = 0
let entry: String = "Detalle"
var body: some View {
VStack {
Divider().frame(maxWidth: .infinity)
.overlay(.black)
HStack(alignment: .center) {
Text(entry)
.fixedSize(horizontal: false, vertical: true)
Spacer()
Image(systemName: "chevron.down")
.foregroundColor(.black)
.padding()
.rotationEffect(.degrees(expanded ? 180 : 360))
.animation(.linear(duration: 0.3), value: expanded)
}.padding(.horizontal)
.padding(.vertical, 6)
.background(.green)
if expanded {
Text("Details")
}
Divider().frame(maxWidth: .infinity)
.overlay(.black)
}.animation(.linear(duration: 0.3), value: expanded)//Animation added
.listRowSeparator(.hidden)
.listRowInsets(.init())
.onTapGesture {
expanded.toggle()
}
}
}
I hope this works for you ;)

SwiftUI List only taps content

I have a List in SwiftUI that I populate with a custom SwiftUI cell, the issue is that on tap I need to do some stuff and the tap only works when you click the text in the cell, if you click any empty space it will not work. How can I fix this?
struct SelectDraftView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var viewModel = SelectDraftViewModel()
var body: some View {
VStack {
List {
ForEach(viewModel.drafts.indices, id: \.self) { index in
DraftPostCell(draft: viewModel.drafts[index])
.contentShape(Rectangle())
.onTapGesture {
presentationMode.wrappedValue.dismiss()
}
}
.onDelete { indexSet in
guard let delete = indexSet.map({ viewModel.drafts[$0] }).first else { return }
viewModel.delete(draft: delete)
}
}
.background(Color.white)
Spacer()
}
}
}
struct DraftPostCell: View {
var draft: CDDraftPost
var body: some View {
VStack(alignment: .leading) {
Text(draft.title ?? "")
.frame(alignment: .leading)
.font(Font(UIFont.uStadium.helvetica(ofSize: 14)))
.padding(.bottom, 10)
if let body = draft.body {
Text(body)
.frame(alignment: .leading)
.multilineTextAlignment(.leading)
.frame(maxHeight: 40)
.font(Font(UIFont.uStadium.helvetica(ofSize: 14)))
}
Text(draft.date?.toString(format: "EEEE, MMM d, yyyy") ?? "")
.frame(alignment: .leading)
.font(Font(UIFont.uStadium.helvetica(ofSize: 12)))
}
.padding(.horizontal, 16)
}
}
try adding .frame(idealWidth: .infinity, maxWidth: .infinity) just after DraftPostCell(...). You can also use a minWidth: if required.
EDIT-1: the code I use for testing (on real devices ios 15.6, macCatalyst, not Previews):
import Foundation
import SwiftUI
struct ContentView: View {
var body: some View {
SelectDraftView()
}
}
class SelectDraftViewModel: ObservableObject {
#Published var drafts: [
CDDraftPost] = [
CDDraftPost(title: "item 1", date: Date(), body: "body 1"),
CDDraftPost(title: "item 2", date: Date(), body: "body 4"),
CDDraftPost(title: "item 3", date: Date(), body: "body 3")]
func delete(draft: CDDraftPost) { }
}
struct CDDraftPost: Codable {
var title: String?
var date: Date?
var body: String?
}
struct SelectDraftView: View {
#Environment(\.presentationMode) var presentationMode
#ObservedObject var viewModel = SelectDraftViewModel()
var body: some View {
VStack {
List {
ForEach(viewModel.drafts.indices, id: \.self) { index in
DraftPostCell(draft: viewModel.drafts[index])
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
.border(.red) // <-- for testing
.onTapGesture {
print("----> onTapGesture")
// presentationMode.wrappedValue.dismiss()
}
}
.onDelete { indexSet in
guard let delete = indexSet.map({ viewModel.drafts[$0] }).first else { return }
viewModel.delete(draft: delete)
}
}
.background(Color.white)
Spacer()
}
}
}
struct DraftPostCell: View {
var draft: CDDraftPost
var body: some View {
VStack(alignment: .leading) {
Text(draft.title ?? "")
.frame(alignment: .leading)
// .font(Font(UIFont.uStadium.helvetica(ofSize: 14)))
.padding(.bottom, 10)
if let body = draft.body {
Text(body)
.frame(alignment: .leading)
.multilineTextAlignment(.leading)
.frame(maxHeight: 40)
// .font(Font(UIFont.uStadium.helvetica(ofSize: 14)))
}
Text(draft.date?.formatted() ?? "")
.frame(alignment: .leading)
// .font(Font(UIFont.uStadium.helvetica(ofSize: 12)))
}
.padding(.horizontal, 16)
}
}
I'm probably late but this will be useful for anyone checking this in the future.
You need to add .background() modifier to your view before you do .onTapGesture{...}
so in your ForEach code would be modified like this:
ForEach(viewModel.drafts.indices, id: \.self) { index in
DraftPostCell(draft: viewModel.drafts[index])
.contentShape(Rectangle())
.frame(maxWidth: .infinity) // you should use the frame parameter according to your needs, but if you want your cell to occupy the whole width of your scroll view, use this one
.background() // this makes the empty portions of view 'non-transparent', so those portions also receive the tap gesture
.onTapGesture {
presentationMode.wrappedValue.dismiss()
}
}
P.S if you need the whole portion of your scroll view cell to receive the tap gesture you'll also need to add .frame(...) modifier, so it has the exact background you want

how to appear a list (using animation) once the button is pressed?

I want once I press the button search
VStack{
Text("Enter friends first name")
.font(.caption)
.fontWeight(.bold)
.foregroundColor(Color("Color"))
TextField("firstname", text: $firstname)
.padding()
.keyboardType(.default)
.background(Color.white)
.autocapitalization(.none)
.textFieldStyle(.roundedBorder)
.shadow(color: Color.gray.opacity(0.1), radius: 5, x: 0, y: 2)
Text("Enter friends last Name")
.font(.caption)
.fontWeight(.bold)
.foregroundColor(Color("Color"))
TextField("lastname", text: $lastname)
.padding()
.keyboardType(.default)
.background(Color.white)
.autocapitalization(.none)
.textFieldStyle(.roundedBorder)
.shadow(color: Color.gray.opacity(0.1), radius: 5, x: 0, y: 2)
Button (action:{
searchUser()
},label:{
Text("Search")
})
}
the list that is in searchUser()that shows the names of friends with this first name and last name and their details appears on the this view under the search button and once the button is pressed but with animation ? thanks
I tried to do the animation but it didn't work. does anyone know how can I do it ?
You can show/hide views conditionally by putting them inside if block.
struct ContentView: View {
#State var shouldShowList = false
var body: some View {
VStack {
if shouldShowList {
VStack {
ForEach(0 ..< 5) { item in
Text("Hello, world!")
.padding()
}
}
}
Button( shouldShowList ? "Hide" : "Show") {
shouldShowList.toggle()
}
}
.animation(.easeInOut, value: shouldShowList) // animation
}
}
Instead,
You can use a view modifier to show/hide.
1. create your own ViewModifire
struct Show: ViewModifier {
let isVisible: Bool
#ViewBuilder
func body(content: Content) -> some View {
if isVisible {
EmptyView()
} else {
content
}
}
}
extension View {
func show(isVisible: Bool) -> some View {
ModifiedContent(content: self, modifier: Show(isVisible: isVisible))
}
}
Usage
struct ContentView: View {
#State var shouldShowList = false
var body: some View {
VStack {
VStack {
ForEach(0 ..< 5) { item in
Text("Hello, world!")
.padding()
}
}
.show(isVisible: shouldShowList) //<= here
Button( shouldShowList ? "Hide" : "Show") {
shouldShowList.toggle()
}
}
.animation(.easeInOut, value: shouldShowList) // animation
}
}

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:

How can I make these SwiftUI text "buttons" change color on tap?

In SwiftUI, how can I make these text "buttons" change color on tap, but revert when you remove your finger?
https://i.imgur.com/WHPGhAT.jpg
Here's what the button code looks like:
LazyVGrid(columns:
Array(repeating:
GridItem(.flexible(),
spacing: 5),
count: 2),
spacing: 2) {
ForEach(viewModel.productIngredients, id: \.self) { ingredient in
Text(ingredient.name)
.font(.system(size: 14))
.fontWeight(.medium)
.foregroundColor(.black)
.padding(8)
.background(RoundedRectangle(cornerRadius: 10).stroke(Color.black, lineWidth: 2))
.padding(.top,5)
/// .background(self.selectedIngredient == ingredient ? Color.blue : Color.white)
.onTapGesture {
self.didTap.toggle()
self.selectedIngredient = ingredient
}
}
}
You can use a custom ButtonStyle to do this:
struct ContentView : View {
var body: some View {
Button(action: {
//Your action code, taken from the previous `onTapGesture` in the original code
//didTap.toggle()
//selectedIngredient = ingredient
}) {
Text("Ingredient")
.fontWeight(.medium)
}.buttonStyle(CustomButtonStyle(isSelected: false)) //could pass a parameter here like isSelected: selectedIngredient == ingredient from your original code
}
}
struct CustomButtonStyle : ButtonStyle {
var isSelected: Bool
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.system(size: 14))
.foregroundColor(.black)
.padding(8)
.background(RoundedRectangle(cornerRadius: 10)
.stroke(configuration.isPressed ? Color.red : Color.black, lineWidth: 2)
)
.padding(.top,5)
//Could also modify style based on isSelected
}
}
Notice that your Text view is now wrapped in a Button and given a buttonStyle of CustomButtonStyle.
Inside CustomButtonStyle, I use a ternary expression to set the color of the background RoundedRectangle based on configuration.isPressed.
I also showed how you could pass in another parameter (isSelected) because in your original example it looked like you may want to do things conditionally based on that as well.
Update with full working example showing columns:
struct Ingredient : Identifiable, Hashable {
var id = UUID()
var name = "Ingredient"
}
struct ContentView: View {
#State var ingredients = [Ingredient(),Ingredient(),Ingredient(),Ingredient(),Ingredient(),Ingredient(),Ingredient(),Ingredient()]
var body: some View {
LazyVGrid(columns:
Array(repeating:
GridItem(.flexible(),
spacing: 5),
count: 2),
spacing: 2) {
ForEach(ingredients, id: \.self) { ingredient in
Button(action: {
//Your action code, taken from the previous `onTapGesture` in the original code
//didTap.toggle()
//selectedIngredient = ingredient
}) {
Text(ingredient.name)
.fontWeight(.medium)
}.buttonStyle(CustomButtonStyle(isSelected: false))
}
}
}
}
struct CustomButtonStyle : ButtonStyle {
var isSelected: Bool
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.system(size: 14))
.foregroundColor(.black)
.padding(8)
.background(RoundedRectangle(cornerRadius: 10)
.stroke(configuration.isPressed ? Color.red : Color.black, lineWidth: 2)
)
.padding(.top,5)
//Could also modify style based on isSelected
}
}

Resources