Hoping someone may know of a solution for this animation issue as I can't find a way to make it work!
Im using ForEach within LazyVStack within ScrollView. I have a .searchable modifier on the scrollview. When I enter/cancel the search field the navigation bar and search field animate upwards/downwards but my scrollview jumps without animation.
if I add .animation(.easeInOut) after .searchable it animates correctly. However there's two issues, its deprecated in iOS 15.0, and it animates the list items in crazy ways as they appear and are filtered etc.
When using a List it also works but can't be customised in the way I need. This issue is present in simulator, in previews and on device.
Does anyone know how I can get this to animate correctly without resorting to using List (Which doesn't have the customisability I need for the list items)?
Thanks for your help!
A slimmed down version of what I'm doing to recreate the issue:
import SwiftUI
struct ContentView: View {
#State var searchText: String = ""
var body: some View {
NavigationView {
ScrollView(.vertical) {
CustomListView()
}
.navigationTitle("Misbehaving ScrollView")
.searchable(text: $searchText, placement: .automatic)
// This .animation() will fix the issue but create many more...
// .animation(.easeInOut)
}
}
}
struct CustomListView: View {
#State private var listItems = ["Item 0", "Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6", "Item 7", "Item 8", "Item 9", "Item 10"]
var body: some View {
LazyVStack(alignment: .leading, spacing: 10) {
ForEach(listItems, id: \.self) { item in
CustomListItemView(item: item)
.padding(.horizontal)
}
}
}
}
struct CustomListItemView: View {
#State var item: String
var body: some View {
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 20, style: .continuous)
.foregroundColor(.green.opacity(0.1))
VStack(alignment: .leading, spacing: 4) {
Text(item)
.font(.headline)
Text(item)
.font(.subheadline)
}
.padding(25)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
An even more basic example that displays the same issue:
import SwiftUI
struct SwiftUIView: View {
#State var text = ""
var body: some View {
NavigationView {
ScrollView {
Text("1")
Text("2")
Text("3")
Text("4")
Text("5")
Text("6")
}
}
.searchable(text: $text)
}
}
struct SwiftUIView_Previews: PreviewProvider {
static var previews: some View {
SwiftUIView()
}
}
We need to animate ScrollView geometry changes synchronously with searchable text field appearance/disappearance, which as seen are animatable.
There are two tasks here: 1) detect searchable state changes 2) animate ScrollView in correct place (to avoid unexpected content animations as already mentioned in question)
A possible solution for task 1) is to read isSearching environment variable:
.background(
// read current searching state is available only in
// child view level environment
SearchingReaderView(searching: $isSearching)
)
// ...
struct SearchingReaderView: View {
#Binding var searching: Bool
#Environment(\.isSearching) private var isSearching
var body: some View {
Text(" ")
.onChange(of: isSearching) {
searching = $0 // << report to perent
}
}
}
and for task 2) is to inject animation right during transition by modifying transaction:
ScrollView(.vertical) {
CustomListView()
}
.transaction {
if isSearching || toggledSearch {
// increased speed to avoid views overlaping
$0.animation = .default.speed(1.2)
// needed to animate end of searching
toggledSearch.toggle()
}
}
Tested with Xcode 13.4 / iOS 15.5 (debug slow animation for better visibility)
Test code on GitHub
Related
I'm experiencing a truly bizarre behavior with an app that has pickers in a view that
calls a map as a sheet modal. The pickers are not directly involved in calling the map -
but set conditions for the annotations that will be displayed. And in the simple example
I have included below, it is clear the issue has nothing to do with the map - the
problem behavior is present with a simple text view in the modal.
The issue: if the user swipes to dismiss the modal it appears to always work as expected.
If the user taps a button to dismiss the modal with either the environment dismiss or
through a binding to the #State that calls the view, then after the second showing of the
modal, you can no longer raise the picker - the displayed value just turns color - as if
a color toggle on tap. I've also tried showing the modal as full screen, as an animation
and as a transition. I get the same result every time. I'm at the point where I think this
must be an Xcode bug, but I hope someone can show me a solution. The same behavior exists
in Preview, Simulator and a real Device.
Here is a very stripped down example which demonstrates the issue:
enum StorageKeys: String {
case appChosenFuel, appChosenState
}//enum
struct ContentView: View {
#State private var showGroupMapView: Bool = false
#State private var showTooBigAlert: Bool = false
#State private var justANumber: Int = 500
var body: some View {
NavigationView {
VStack {
Text("Picker plus Sheet Test")
.padding()
FuelPickerView()
}
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Button {
if justANumber > 1000 {
showTooBigAlert = true
} else {
withAnimation {
showGroupMapView.toggle()
}
}
} label: {
Image(systemName: "map")
.font(.system(size: 20))
.frame(width: 40, height: 40)
}
.sheet(isPresented: $showGroupMapView, onDismiss: {
print("you dismissed me")
}) {
GroupMapView()
}
}//toolbar group
}//toolbar
}//nav
}//body
}//sruct
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct GroupMapView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
ZStack {
Text("This is the map view")
VStack {
HStack {
Spacer()
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Image(systemName: "gear")
.padding(.trailing, 20)
.padding(.top, 20)
}
}
Spacer()
}
}//z
}
}//struct
class FuelPickerViewModel: ObservableObject {
struct FuelItem: Identifiable, Hashable {
let id = UUID()
let name: String
let initials: String
}
#Published var fuelItems = [
FuelItem(name: "Biodiesel (B20 and above)", initials: "BD"),
FuelItem(name: "Compressed Natural Gas", initials: "CNG"),
FuelItem(name: "Ethanol (E85)", initials: "E85"),
FuelItem(name: "Electric", initials: "ELEC"),
FuelItem(name: "Hydrogen", initials: "HY"),
FuelItem(name: "Liquified Natural Gas", initials: "LNG"),
FuelItem(name: "Liquified Petroleum Gas (Propane)", initials: "LPG")
]
}//class
struct FuelPickerView: View {
#AppStorage(StorageKeys.appChosenFuel.rawValue) var appChosenFuel = "ELEC"
#AppStorage(StorageKeys.appChosenState.rawValue) var appChosenState = "CO"
#StateObject var fuelPickerVM = FuelPickerViewModel()
var body: some View {
return VStack {
Picker("Fuel", selection: $appChosenFuel) {
ForEach(fuelPickerVM.fuelItems, id: \.self) {
Text($0.initials)
}
}
}
}//body
}//struct
And after the second modal display/dismiss with the button, tapping the picker does
nothing except change the background color:
Any guidance would be appreciated: Xcode 13.2.1 iOS 15.2
I spent 14 hours trying to figure it out today, I went over here, googled, and watched videos but nothing helped. I gave up so I decided to ask a question.
Typically, I have two views in one, such as the list on the left and details on the right. On the iPhone, I was able to use the sheet to pop up the second view without any issues, but on the iPad, I have a second view on the right side and it does not update when I click on the list. I tried #Binging and #State, but it didn't work.
How can I pass data from one view to another?
The navigation link code:
let navigationItemList: [NavigationItems] = [NavigationItems(image: Image(systemName: "hourglass"), title: "Feature 1", subtitle: "Subtitle", linkView: AnyView(FeatureView1())),
NavigationItems(image: Image(systemName: "clock.arrow.2.circlepath"), title: "Feature 2", subtitle: "Subtitle", linkView: AnyView(FeatureView2()))]
struct ContentView: View {
var body: some View {
NavigationView {
List {
Section(footer: MainFooterView()) {
ForEach(navigationItemList) { items in
HStack {
items.image?
.frame(width: 30, height: 30)
.font(.system(size: 25))
.foregroundColor(color3)
Text("")
NavigationLink(items.title, destination: items.linkView)
}
.listRowBackground(Color.clear)
.listRowSeparatorTint(.clear)
}
.navigationTitle("Features")
.listStyle(.insetGrouped)
}
}
}
}
}
First view:
struct FeatureView1 : View {
var body: some View {
HStack {
List(item) { items in
Button("Click to update title in Feature View 2") {
FeatureView2(title: "Button Called") //Nothing happened
}
}
FeatureView2()
}
}
Second view:
var body: some View {
var title = ""
ZStack {
Text(title) //When I click a button from the list, it should show "Button Called", but nothing happens.
}
}
Before automating it, try to make a simple example like that to understand how to share datas between views:
(As you can see, destination view take the title as parameter)
struct ContentView: View {
var body: some View {
NavigationView {
NavigationLink(destination: FeatureView1(title: "FeatureView1")) {
Text("Go to FeatureView1")
}
}
}
}
struct FeatureView1 : View {
var title: String
var body: some View {
Text(title)
NavigationLink(destination: FeatureView2(title: "FeatureView2")) {
Text("Go to FeatureView2")
}
}
}
struct FeatureView2 : View {
var title: String
var body: some View {
Text(title)
}
}
There are many other ways to share datas between views according to your use case, I let you see #EnvironmentObject, #Binding etc later
I am trying to do programmatic navigation in NavigationView, but for some reason I am unable to switch between the views. When switching from the parent view everything works fine - but as soon as I am trying to switch while being in one of the child views I get this strange behaviour (screen is switching back and forth). I tried disabling animations, but this did not help. Strangely enough, if I remove a list together with .navigationViewStyle(StackNavigationViewStyle()) everything starts to work - but I need a list.
This seems to be somewhat similar to Deep programmatic SwiftUI NavigationView navigation but I do not have deep nesting and it still does not work.
I am using iOS 14.
struct TestView: View {
#State private var selection: String? = nil
var body: some View {
VStack {
NavigationView {
VStack {
List {
NavigationLink(destination: Text("View A"), tag: "A", selection: self.$selection) { Text("A") }
NavigationLink(destination: Text("View B"), tag: "B", selection: self.$selection) { Text("B") }
}
}
.navigationTitle("Navigation")
}
.navigationViewStyle(StackNavigationViewStyle())
Button("Tap to show A") {
selection = "A"
}.padding()
Button("Tap to show B") {
selection = "B"
}.padding()
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestView()
}
}
Here is the behaviour i get:
Navigation View/Link is meant to operate from parent to child directly, if you break that order then you should not use navigate via NavLink.
What you need to do is use a fullScreenCover which I think solves your problem nicely. Copy and paste the code to see what I mean.
import SwiftUI
struct TestNavView: View {
#State private var selection: String? = nil
#State private var isShowing = false
#Environment(\.presentationMode) var pMode
var body: some View {
VStack {
NavigationView {
VStack {
List {
NavigationLink(destination: Text("View A"), tag: "A", selection: self.$selection) { Text("A") }
NavigationLink(destination: Text("View B"), tag: "B", selection: self.$selection) { Text("B") }
}.fullScreenCover(isPresented: $isShowing, content: {
CView()
})
}
.navigationTitle("Navigation")
}
.navigationViewStyle(StackNavigationViewStyle())
Button("Tap to show A") {
selection = "A"
}.padding()
Button("Tap to show B") {
isShowing = true
selection = "B"
}.padding()
Button("Tap to show C") {
isShowing = true
}.padding()
}
}
}
struct TestView_Previews: PreviewProvider {
static var previews: some View {
TestNavView()
}
}
struct CView: View {
#Environment(\.presentationMode) var pMode
var body: some View {
VStack {
Button("Back") {self.pMode.wrappedValue.dismiss() }
Spacer()
Text("C")
Spacer()
}
}
}
If you are only wanting the presented view to take up half the screen, I would recommend using a ZStack to present the view over top of the main window.
You can add your own custom back button to the top left corner (or elsewhere).
This would allow both views to presented and switched between easily.
You can also add a withAnimation() to have the overlayed views to present nicely.
I'm trying to embed a view that contains a Header view and a List The header and list render in the original view, but the list completely disappears when I put it in the parent view. I don't understand what I'm doing wrong. I tried putting the header in the list, but then the entire view disappears.
Here is the code for the view that contains the list:
import SwiftUI
struct CityDetailCategoryView: View {
let cityDetailViewModel: CityDetailViewModel
let titles = ["Category 2", "Category 3", "Category 4", "Category 5", "Category 6", "Category 7"]
var body: some View {
VStack(alignment: .leading) {
SectionHeadingView(cityDetailViewModel: CityDetailViewModel, sectionTitle: "Categories")
List {
ForEach(titles, id: \.self) { title in
Text(title)
.font(.title3)
.fontWeight(.medium)
}
}
}
}
}
struct CityDetailCategoryView_Previews: PreviewProvider {
static var previews: some View {
CityDetailCategoryView(cityDetailViewModel: CityDetailViewModel())
.previewLayout(.sizeThatFits)
}
}
...and the code of the parent view:
import SwiftUI
struct CityDetailView: View {
#ObservedObject var cityDetailViewModel: CityDetailViewModel
var body: some View {
NavigationView {
ScrollView(.vertical, showsIndicators: false, content: {
VStack(alignment: .leading, spacing: 6, content: {
DetailHeaderImageView(cityDetailViewModel: CityDetailViewModel)
Group {
CityDetailTitleView(cityDetailViewModel: CityDetailViewModel)
CityDetailQuickFactsView(cityDetailViewModel: CityDetailViewModel)
CityDetailCategoryView(cityDetailViewModel: CityDetailViewModel)
CityDetailMiscellaneousView(cityDetailViewModel: CityDetailViewModel)
HStack {
Spacer()
Text("Data provided by ****")
.font(.caption2)
Spacer()
}
.padding()
}
.padding(.horizontal, 24)
})
})
.edgesIgnoringSafeArea(.top)
.navigationBarTitle(cityDetailViewModel.cityDetail.name, displayMode: .inline)
.navigationBarHidden(true)
}
}
}
struct CityDetailView_Previews: PreviewProvider {
static var previews: some View {
CityDetailView(cityDetailViewModel: CityDetailViewModel())
}
}
image of the view containing the list:
image of the parent view:
You're embedding a scrolling view (List) inside another scrolling view (ScrollView), which is always going to have some inherent difficulties (and potentially strange UI/UX consequences with scrolling).
Because the CityDetailCategoryView doesn't have any inherent height besides its header (the rest can be collapsed into the ScrollView), it collapses to zero.
You can use frame to set a height for it, as shown in this simplified example (if you remove the frame call, it behaves the same as yours):
var items = [1,2,3,4,5,6]
struct ContentView: View {
var body: some View {
ScrollView {
ForEach(items, id: \.self) { item in
Text("item")
}
List {
ForEach(items, id: \.self) { item in
Text("item")
}
}.frame(height: 200) //<-- here
Text("last item")
}
}
}
That being said, I think you'll still run the risk of funny scrolling behavior.
My suggestion would be to rearchitect your views so that you have one list or scrolling view for all of the content.
I'm trying to create a TextField with a line below it, for that I used a Divide as suggested in this answer and I added a .onTapGesture() to it to change the Divider color, however that's a component that's embedded in another view.
In my ContentView I have a Button and my UnderscoredTextField and they are contained inside a HStack and it's contained inside a Background component (taken from this answer) to be able to dismiss the keyboard programmatically, but I'd like to be able to change the #State var isActive variable inside the UnderscoredTextField when that happens, but as I'm new to SwiftUI I'm not quite sure how to achieve this.
import SwiftUI
struct ContentView: View {
var body: some View {
Background {
UnderscoredTextField(phoneNumber: "")
}.onTapGesture {
self.hideKeyboard()
//How to tell my UnderscoreTextField's isActive variable to change
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
import SwiftUI
struct UnderscoredTextField: View {
#State var phoneNumber: String
#State var isActive: Bool = false
var body: some View {
VStack {
TextField("12345678", text: $phoneNumber)
.keyboardType(.phonePad)
.onTapGesture {
self.isActive = true
}
Divider()
.padding(.horizontal)
.frame(height: 1)
.background(isActive ? Color.red : Color.gray)
}
}
}
struct UnderscoredTextField_Previews: PreviewProvider {
static var previews: some View {
UnderscoredTextField(phoneNumber: "12345678")
}
}
This is what it looks like when I hide the keyboard, but I'd like to switch it back to gray
If I understood correctly it is enough to use onEditingChanged, like below
var body: some View {
VStack {
TextField("12345678", text: $phoneNumber, onEditingChanged: {
self.isActive = $0 // << here !!
}).keyboardType(.phonePad)
Divider()
.padding(.horizontal)
.frame(height: 1)
.background(isActive ? Color.red : Color.gray)
}
}