Adding Shadow to LazyHGrid Element Selection - swiftui

I have an app in which the user may select a SF icon from those within a LazyHGrid. I would like to add a shadow around the selected icon and remove the shadow when deselected.
Currently, the working code below may be used to scroll the available icons and select an icon by tapping. I need help changing the view to support applying shadow to the selected element.
I tried placing the same image() and modifiers within the button action but got a Xcode warning that the ZStack initializer is unused. I also tried adding a shadow modifier to the view changing the shadow parameters with state properties set in the button action area. This applied shadow to all elements in LazyHGrid. I want the shadow applied only to the selected element.
struct ImageStore: Identifiable, Hashable {
var iconName: String
var id: Int
}
struct ContentView: View {
let rows = [
GridItem(.flexible()),
]
let colors: [Color] = [.green, .red, .yellow, .blue]
let imageName = [
ImageStore(iconName: "a.square.fill", id: 0),
ImageStore(iconName: "b.square.fill", id: 1),
ImageStore(iconName: "c.square.fill", id: 2),
ImageStore(iconName: "d.square.fill", id: 3),
ImageStore(iconName: "e.square.fill", id: 4),
ImageStore(iconName: "f.square.fill", id: 5),
ImageStore(iconName: "g.square.fill", id: 6),
]
#State private var selectedIcon: Int = 0
var body: some View {
VStack {
ScrollView (.horizontal) {
LazyHGrid( rows: rows, spacing: 20) {
ForEach(imageName, id: \.self) { image in
Button( action: {
selectedIcon = image.id
print("image name = \(image.iconName)")
print("id = \(image.id)")
print("selectedIcon = \(selectedIcon)")
}){
Image(systemName: image.iconName)
.font(.largeTitle)
.foregroundColor(.white)
.padding()
.background(colors[image.id % colors.count])
}
}
}
}
}
}
}

Perhaps I'm not understanding your question fully, but it should be as simple as using the .shadow modifier with a ternary expression, e.g.
.shadow(radius: selectedIcon == image.id ? 5 : 0)
to make sure the image doesn't have it's own shadow in addition to the background, add a .drawingGroup modifier, e.g
Button {
selectedIcon = image.id
} label: {
Image(systemName: image.iconName)
.font(.largeTitle)
.foregroundColor(.white)
.padding()
.background(colors[image.id % colors.count])
.drawingGroup()
.shadow(radius: selectedIcon == image.id ? 5 : 0)
}

Related

How to tap on a grid item in a LazyVGrid and segue to another view (Essentially like a collection view)

I have my code set as follows:
var body: some View {
NavigationStack {
VStack {
// Contacts Scroll View
ScrollView {
LazyVGrid(columns: threeColumnGrid, spacing: 20) {
ForEach($contacts, id: \.self) { $contact in
ContactCell(firstName: $contact.firstName.wrappedValue)
}
}.padding(EdgeInsets(top: 20,
leading: 20,
bottom: 20,
trailing: 20))
}
}.background(Color(CustomColors.background.rawValue))
}
}
I would like to be able to tap on one of the grid items in order to segue into another screen, but the only solution I can come up with is NavigationLink which only inserts a link that needs to be tapped.
I need the entire grid item to be tappable without any extra text acting as a link.
Side note: I have also looked into the isActive property of NavigationLink, which worked great, but this is being deprecated in iOS 16... It's as if Apple refuses to allow us to create a collection view using swiftUI.
Figured it out. I used: navigationDestination(isPresented:destination:)
See code below:
struct ContactsGridView: View {
#Binding var contacts: [Contact]
#State var shouldPresentContactMainView = false
let threeColumnGrid = [GridItem(.flexible(), spacing: 20),
GridItem(.flexible(), spacing: 20),
GridItem(.flexible(), spacing: 20)]
var body: some View {
NavigationStack {
VStack {
// Contacts Scroll View
ScrollView {
LazyVGrid(columns: threeColumnGrid, spacing: 20) {
ForEach($contacts, id: \.self) { $contact in
ContactCell(firstName: $contact.firstName.wrappedValue)
.onTapGesture {
shouldPresentContactMainView = true
}
.navigationDestination(isPresented: $shouldPresentContactMainView) {
ContactMainView()
}
}
}.padding(EdgeInsets(top: 20,
leading: 20,
bottom: 20,
trailing: 20))
}
}.background(Color(CustomColors.background.rawValue))
}
}
}

Align Subtitles and Footers with LazyVGrid?

I have a question about using LazyVGrid to align the subtitles and footers for each column. Say I have a view like this:
Title
Subtitle A Subtitle B Subtitle C
row1A row1B row1C
row2A row2B row2C
row3A row3B row3C
row4A row4B row4C
row5A row5B row5C
Total A Total B Total C
Currently I use Geometry Reader to display the headers and footers and LazyVGrid to display the data. I have noticed that if I line up the subtitles with a 12 ProMax simulator then view my setup on the 12 Mini simulator that the Mini titles have moved and don't appear very professional. Is it possible to use something like the following with LazyVGrid to not only display the data but also the subtitles and footers? Using pinnedViews: [] I can display the first column header, but how do I display all three subtitles and footers? There is some Apple documentation here on using section headers but nothing on multiple section headers and footers.
private var columns: [GridItem] = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
GeometryReader { g in
ScrollView {
VStack (alignment: .leading) {
ShowTitle()
ShowSubTitle()
LazyVGrid(
columns: columns,
alignment: .leading,
spacing: 10, // vertical spacing
pinnedViews: [.sectionHeaders, .sectionFooters]
) {
Section(header: Text("Subtitle A")
.font(.headline)) {
ForEach(0..<self.categories.catItem.count, id: \.self) { index in
ShowRow(index: index)
}
}
}
I mocked this up off of the Apple example at Grouping Data with Lazy Stack Views. The trick is to put the LazyVStack inside of the ForEach that goes through each section like this:
struct LazyVGridSectioned: View {
let sections = [
ColorData(color: .red, name: "Reds", footerInfo: "Red Footer Info"),
ColorData(color: .green, name: "Greens", footerInfo: "Green Footer Info"),
ColorData(color: .blue, name: "Blues", footerInfo: "Blue Footer Info")
]
var body: some View {
ScrollView {
HStack {
ForEach(sections) { section in
LazyVStack(spacing: 1, pinnedViews: [.sectionHeaders, .sectionFooters]) {
Section(header: SectionHeaderView(colorData: section), footer: SectionFooterView(colorData: section)) {
ForEach(section.variations) { variation in
section.color
.brightness(variation.brightness)
.frame(height: 20)
}
}
}
}
}
}
}
}
struct SectionHeaderView: View {
var colorData: ColorData
var body: some View {
HStack {
Spacer()
Text("Header for \(colorData.name)")
.font(.headline)
.foregroundColor(colorData.color)
Spacer()
}
.padding()
.background(Color.primary
.colorInvert()
.opacity(0.75))
}
}
struct SectionFooterView: View {
var colorData: ColorData
var body: some View {
HStack {
Spacer()
Text(colorData.footerInfo)
.font(.headline)
.foregroundColor(colorData.color)
Spacer()
}
.padding()
.background(Color.primary
.colorInvert()
.opacity(0.75))
}
}
struct ColorData: Identifiable {
let id = UUID()
let name: String
let footerInfo: String // Add parameter in ColorData
let color: Color
let variations: [ShadeData]
struct ShadeData: Identifiable {
let id = UUID()
var brightness: Double
}
// Add it to the init as well
init(color: Color, name: String, footerInfo: String) {
self.name = name
self.color = color
self.footerInfo = footerInfo // Assign it here
self.variations = stride(from: 0.0, to: 0.5, by: 0.1)
.map { ShadeData(brightness: $0) }
}
}

How to recreate the grid (in screenshot) in SwiftUI without breaking navigation?

I am trying to recreate a layout similar to the Reminders app. Looking at it makes me think it was built with SwiftUI. I also believe Apple mentioned so in one of the WWDC videos (can't remember which one).
This above screenshot seems to be a List, with a LazyVGrid as the first View inside the List. Tapping on each of the items in the LazyVGrid, such as Today, Scheduled, All and Flagged, navigates to the relevant screen, which means they are all NavigationLinks. Also note that the LazyVGrid has 2 columns.
And then there is another section "My Lists" which has rows which look like regular list rows in a List with style .insetGrouped. Also, every item in this Section is a NavigationItem, and thus comes with the disclosure indicator on the right as usual. Recreating this is trivial, so it has been left out from the MRE.
I am having trouble recreating the first section, which has that LazyVGrid. I faced 3 problems (as mentioned in the image), of which I have been able to solve the first one only. The other two problems remain. I want to know if this MRE can be fixed, or is my entire approach incorrect.
I am including a minimum reproducible example below.
import SwiftUI
#main
struct TestApp: App {
var body: some Scene {
WindowGroup {
RemindersView()
}
}
}
struct RemindersView: View {
private var columns: [GridItem] = [GridItem(.adaptive(minimum: 150))]
private var smartLists: [SmartList] = SmartList.sampleLists
var body: some View {
NavigationView {
List {
Section(header: Text("Using LazyVGrid")) {
grid
}
Section(header: Text("Using HStack")) {
hstack
}
}
.navigationTitle("Store")
}
.preferredColorScheme(.dark)
}
private var grid: some View {
LazyVGrid(columns: columns, spacing: 8) {
ForEach(smartLists) { smartList in
// This use of **ZStack with an EmptyView with opacity 0** is a hack being used to avoid the disclosure indicator on each item in the grid
ZStack(alignment: .leading) {
NavigationLink( destination: SmartListView(list: smartList)) {
EmptyView()
}
.opacity(0)
SmartListView(list: smartList)
}
}
}
.listRowInsets(EdgeInsets())
.listRowBackground(Color.clear)
}
private var hstack: some View {
ScrollView(.horizontal) {
HStack {
ForEach(smartLists) { smartList in
NavigationLink(destination: SmartListView(list: smartList)) {
SmartListView(list: smartList)
}
.buttonStyle(.plain)
}
}
}
.listRowInsets(EdgeInsets())
.listRowBackground(Color.clear)
}
}
struct RemindersView_Previews: PreviewProvider {
static var previews: some View {
RemindersView()
}
}
struct SmartList: Identifiable {
var id: UUID = UUID()
var title: String
var count: Int
var icon: String
var iconColor: Color
static var sampleLists: [SmartList] {
let today = SmartList(title: "Today", count: 5, icon: "20.circle.fill", iconColor: .blue)
let scheduled = SmartList(title: "Scheduled", count: 12, icon: "calendar.circle.fill", iconColor: .red)
let all = SmartList(title: "All", count: 77, icon: "tray.circle.fill", iconColor: .gray)
let flagged = SmartList(title: "Flagged", count: 5, icon: "flag.circle.fill", iconColor: .orange)
return [today, scheduled, all, flagged]
}
}
struct SmartListView: View {
var list: SmartList
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .center) {
Image(systemName: list.icon)
.renderingMode(.original)
.font(.title)
.foregroundColor(list.iconColor)
Spacer()
Text("\(list.count)")
.font(.system(.title, design: .rounded))
.fontWeight(.bold)
.padding(.horizontal, 8)
}
Text(list.title)
.font(.system(.headline, design: .rounded))
.foregroundColor(.secondary)
}
.padding(8)
.background(
RoundedRectangle(cornerRadius: 12)
.foregroundColor(.gray.opacity(0.25))
)
.padding(2)
.frame(minWidth: 150)
}
}
EDIT 1: Adding video demo of what editing the dynamic Grid looks like and how the Grid has dynamic grid items (via the Edit button at the top right): https://imgur.com/a/TV0kifY

SwiftUI: Custom label in navigation link is greyed out

I'm trying to use my custom card view as label to navigation link. this navigation link is also a Grid item.
The result is a
My Card Code:
struct CityCard: View {
var cityData: CityData
var body: some View {
VStack {
Text("\(cityData.name)")
.padding([.top], 10)
Spacer()
Image(systemName: cityData.forecastIcon)
.resizable()
.frame(width: 45, height: 45)
Spacer()
HStack(alignment: .bottom) {
Text(String(format: "%.2f $", cityData.rate))
.padding([.leading, .bottom], 10)
Spacer()
Text(String(format: "%.2f °", cityData.degrees))
.padding([.trailing, .bottom], 10)
}
}.frame(width: 150, height: 150)
.background(Color.blue)
.cornerRadius(10)
.navigationTitle(cityData.name)
}
}
My List View:
struct CityList: View {
var cities: [CityData]
let columns = [
GridItem(.flexible(minimum: 150, maximum: 150)),
GridItem(.flexible(minimum: 150, maximum: 150))
]
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(self.cities) { item in
NavigationLink(destination: Text(item.name), label: {
CityCard(cityData: item)
})
}
}
}
}
}
someone has a solution why it gives me this opacity?
Update:
The main contact view is:
It's greyed out because you don't yet have a NavigationView in your view hierarchy, which makes it possible to actually navigate to a new view.
You can, for example, wrap your ScrollView in a NavigationView:
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: columns, spacing: 20) {
ForEach(self.cities) { item in
NavigationLink(destination: Text(item.name), label: {
CityCard(cityData: item)
})
}
}
}
}
}
Or, if it's only happening in your preview, add a NavigationView to your preview code:
NavigationView {
CityList(...)
}
Keep in mind that if you have the default accent color (blue, in light mode) that your links will become invisible (since they will be blue) on top of the blue background. You can set a custom accent color in your project or via the accentColor modifier:
NavigationView {
//content
}.accentColor(.black)

How to Remove Padding/Align Picker Value Left with Hidden Label in SwiftUI?

I'm trying to align my Picker hidden label values left to another TextField in my form (see attached image). What's the best way to remove the 8px padding in the displayed picker value?
import SwiftUI
struct ContactFormView: View {
var countries = ["Malaysia", "Singapore", "Japan"]
#State var name: String = ""
#State var mobile: String = ""
#State var mobileCountry: String = "Malaysia"
var body: some View {
NavigationView {
Form {
Section(header: Text("Details")) {
TextField("Name", text: $name)
.border(Color.red, width: 1)
HStack {
Picker(selection: $mobileCountry, label: EmptyView()) {
ForEach(countries, id: \.self) {
Text($0).border(Color.red)
}
}
.scaledToFit()
.labelsHidden()
.border(Color.red, width: 1)
TextField("Mobile", text: $mobile)
.border(Color.red, width: 1)
}
}
}
.navigationBarTitle("New contact")
}
}
}
Probably your best bet is to use a negative padding on your picker:
Picker(selection: $mobileCountry, label: EmptyView()) {
ForEach(countries, id: \.self) {
Text($0).border(Color.red)
}
}
.scaledToFit()
.labelsHidden()
.border(Color.red, width: 1)
.padding(.horizontal, -8)
Loading your code in Xcode 12.5, adding that negative padding aligns the text in the way you want.
Intuitively I would have chosen .padding(.leading, -8) to remove padding for the Picker's leading edge only – but in doing so, the gap between the picker and text field grows larger.
Applying the negative padding to both horizontal values keeps that gap the same for me. But I'd recommend in your code trying both, and seeing which one makes the most sense for you.