I'm trying to animate the hiding/showing of a picker based on a toggle. If true or false, I would like the picker to easeInOut.
I've tried adding the .animation(Animation.easeInOut(duration: 0.5)) to the picker itself or the HStack the picker is in, but both add the animation to values inside the picker and when scrolling through the values the application to crashes.
HStack {
if showPicker {
Picker(selection: $selected.value, label: Text(selected.type)) {
ForEach(digits, id: \.self) { d in
Text("\(d)")
}
}
.frame(width: 40)
}
}
.animation(Animation.easeInOut(duration: 2))
if showPicker {
Picker(selection: $selected.value, label: Text(selected.type)) {
ForEach(digits, id: \.self) { d in
Text("\(d)")
}
}
.frame(width: 40)
.animation(Animation.easeInOut(duration: 0.5))
}
Both options do animate the hiding/showing the picker, but it also animates scrolling through the values in the picker, which causes it to crash.
Any help would be appreciated.
Thank you
About your first approach, putting animation on HStack. Never do that. According to the comments in the declaration file:
Use this modifier on leaf views rather than container views. The
animation applies to all child views within this view; calling
animation(_:) on a container view can lead to unbounded scope.
I tried your second approach (filling the missing bits from your post), and it won't crash. Maybe you can update your question with a fully reproducible example.
Changed animation to explicit, so other parameters are not affected:
struct PickerStackOverflow: View {
#State private var showPicker = true
#State private var value: Int = 1
let digits: [Int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
var body: some View {
VStack {
Text("Selected \(value)")
HStack {
if showPicker {
Picker(selection: $value, label: Text("Label")) {
ForEach(digits, id: \.self) { d in
Text("\(d)")
}
}
.frame(width: 40)
}
}
Button("Tap Me") {
withAnimation(Animation.easeInOut(duration: 2)) {
self.showPicker.toggle()
}
}
}
}
}
Related
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)
}
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))
}
}
}
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
I have a CreateNewCard View that is opened in a sheet. In this view I have the KeyboardHelperView that should be displayed if showHelp is true. This view contains a TabView. But when I run this code I get an Simultaneous accesses to XX, but modification requires exclusive access. The weird part now is: When I change the TabView() to List it works without any problem. Is this a SwiftUI bug? Does anyone know how to fix it?
struct CreateNewCard: View {
#State var showHelp = false
var body: some View {
ZStack {
Color(.blue).edgesIgnoringSafeArea(.all)
Text("open")
.onTapGesture {
withAnimation(.spring()){
self.showHelp = true
}
}
KeyboardHelpView(show: $showHelp)
.frame(width: 300, height: 500)
.opacity(showHelp ? 1 : 0)
}
}
}
struct KeyboardHelpView: View {
#Binding var show: Bool
var steps = ["A", "B", "C", "D","E"]
var images = ["1", "2", "3", "4", "5"]
var body: some View {
ZStack(alignment: .topTrailing){
Color(.red)
TabView() {
ForEach(steps.indices) { index in
VStack(alignment: .center, spacing: 10){
Text(steps[index])
Text(images[index])
}
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
Image(systemName: "xmark.circle.fill")
.padding()
.foregroundColor(Color(.red))
.font(.system(size: 30, weight: .semibold))
.onTapGesture {
withAnimation(Animation.spring()){
self.show = false
}
}
}
.cornerRadius(20)
.background(
RoundedRectangle(cornerRadius: 20)
.shadow(color: Color.black.opacity(0.3), radius: 5, x: 5, y: 5)
)
}
}
EDIT: Okay, the next weird observations: It works on a real device without any problem. And when I comment out .tabViewStyle(PageTabViewStyle(indexDisplayMode: .always)) it works on the simulator as well. I'm not sure whether this is now a Xcode/simulator bug or is this just a random behavior?!
I have a Plus button in my first view. Looks like a FAB button. I want to hide it after I tap some step wrapped in NavigationLink. So far I have something like this:
ForEach(0 ..< 12) {item in
NavigationLink(destination: TransactionsDetailsView()) {
VStack {
HStack(alignment: .top) {
Text("List item")
}
.padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
.foregroundColor(.black)
Divider()
}
}
.simultaneousGesture(TapGesture().onEnded{
self.showPlusButton = false
})
.onAppear(){
self.showPlusButton = true
}
}
It works fine with single tap. But when I long press NavigationLink it doesn't work. How should I rewrite my code to include long press as well? Or maybe I should make it work different than using simultaneousGesture?
I'm using the following code. I prefer it to just NavigationLink by itself because it lets me reuse my existing ButtonStyles.
struct NavigationButton<Destination: View, Label: View>: View {
var action: () -> Void = { }
var destination: () -> Destination
var label: () -> Label
#State private var isActive: Bool = false
var body: some View {
Button(action: {
self.action()
self.isActive.toggle()
}) {
self.label()
.background(
ScrollView { // Fixes a bug where the navigation bar may become hidden on the pushed view
NavigationLink(destination: LazyDestination { self.destination() },
isActive: self.$isActive) { EmptyView() }
}
)
}
}
}
// This view lets us avoid instantiating our Destination before it has been pushed.
struct LazyDestination<Destination: View>: View {
var destination: () -> Destination
var body: some View {
self.destination()
}
}
And to use it:
var body: some View {
NavigationButton(
action: { print("tapped!") },
destination: { Text("Pushed View") },
label: { Text("Tap me") }
)
}
Yes, NavigationLink does not allow such simultaneous gestures (might be as designed, might be due to issue, whatever).
The behavior that you expect might be implemented as follows (of course if you need some chevron in the list item, you will need to add it manually)
struct TestSimultaneousGesture: View {
#State var showPlusButton = false
#State var currentTag: Int?
var body: some View {
NavigationView {
List {
ForEach(0 ..< 12) { item in
VStack {
HStack(alignment: .top) {
Text("List item")
NavigationLink(destination: Text("Details"), tag: item, selection: self.$currentTag) {
EmptyView()
}
}
.padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
.foregroundColor(.black)
Divider()
}
.simultaneousGesture(TapGesture().onEnded{
print("Got Tap")
self.currentTag = item
self.showPlusButton = false
})
.simultaneousGesture(LongPressGesture().onEnded{_ in
print("Got Long Press")
self.currentTag = item
self.showPlusButton = false
})
.onAppear(){
self.showPlusButton = true
}
}
}
}
}
}
Another alternative I have tried. Not using simultaneousGesture, but an onDisappear modifier instead. Code is simple and It works. One downside is that those actions happen with a slight delay. Because first the destination view slides in and after this the actions are performed. This is why I still prefer #Asperi's answer where he added .simultaneousGesture(LongPressGesture) to my code.
ForEach(0 ..< 12) {item in
NavigationLink(destination: TransactionsDetailsView()) {
VStack {
HStack(alignment: .top) {
Text("List item")
}
.padding(EdgeInsets(top: 5, leading: 10, bottom: 5, trailing: 10))
.foregroundColor(.black)
Divider()
}
}
.onDisappear(){
self.showPlusButton = false
}
.onAppear(){
self.showPlusButton = true
}
}
I have tried an alternative approach to solving my problem. Initially I didn't use "List" because I had a problem with part of my code. But it cause another problem: PlusButton not disappearing on next screen after tapping NavigationLink. This is why I wanted to use simultaneousGesture - after tapping a link some actions would be performed as well (here: PlusButton would be hidden). But it didn't work well.
I have tried an alternative solution. Using List (and maybe I will solve another problem later.
Here is my alternative code. simultaneousGesture is not needed at all. Chevrons are added automatically to the list. And PlusButton hides the same I wanted.
import SwiftUI
struct BookingView: View {
#State private var show_modal: Bool = false
var body: some View {
NavigationView {
ZStack {
List {
DateView()
.listRowInsets(EdgeInsets())
ForEach(0 ..< 12) {item in
NavigationLink(destination: BookingDetailsView()) {
HStack {
Text("Booking list item")
Spacer()
}
.padding()
}
}
}.navigationBarTitle(Text("Booking"))
VStack {
Spacer()
Button(action: {
print("Button Pushed")
self.show_modal = true
}) {
Image(systemName: "plus")
.font(.largeTitle)
.frame(width: 60, height: 60)
.foregroundColor(Color.white)
}.sheet(isPresented: self.$show_modal) {
BookingAddView()
}
.background(Color.blue)
.cornerRadius(30)
.padding()
.shadow(color: Color.black.opacity(0.3), radius: 3, x: 3, y: 3)
}
}
}
}
}