How to perform animated transitions of a view inside a ScrollView? - swiftui

I have a ScrollView, which contains an HStack, which contains a ForEach, which contains some custom view with an attached transition. It looks something like:
ScrollView {
HStack {
ForEach(self.objects) { object in
ObjectView(object)
.transition(.slide)
}
}
}
When I update the contents of self.objects (within a call to withAnimation), I want the old/new ObjectViews to animate in and out as specified by the transition.
Unfortunately, no animated transitions appear. If I remove the ScrollView (leaving the HStack to be the root view), the animated transitions work as expected.
Is there a way I can animate the transitions of views inside a ScrollView?

maybe you are interested...this works:
struct ObjectView : View {
var text: String
var body: some View {
Text(text)
}
}
struct ContentView: View {
#State var objects = ["a", "b", "c"]
var body: some View {
Group() {
Button("add") {
withAnimation() {
self.objects.append("f")
}
}
ScrollView {
HStack {
ForEach(objects, id: \.self) { object in
ObjectView(text: object)
.transition(.slide)
}
}.frame(width: 500)
}
}
}
}

Related

The searchable modifier on tvOS, when inside a NavigationView, doesn't allow refocusing the search bar

When using a NavigationView and a ScrollView with searchable, as soon as you focus a item in the LazyVGrid the search bar collapses the keyboard, and it's no longer possible to re-focus the search bar to change the query.
It doesn't matter if the .searchable modifier is applied to the ScrollView or the NavigationView.
The more I look at it, the more it appears to be a SwiftUI bug on tvOS, but I would still like to find a workaround, if possible.
Sample code which reproduces the problem:
import SwiftUI
struct ContentView: View {
private var fruits = ["Apples", "Pears", "Oranges", "Plums", "Pineapples", "Bananas"]
#State private var items: [String]
#State private var searchText: String = ""
init() {
self.items = fruits
}
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 300))], spacing: 40) {
ForEach(items, id: \.self) { item in
NavigationLink(destination: DetailView(text: item)) {
Text(item)
}
}
}
}
.searchable(text: $searchText)
.onChange(of: searchText) { query in
if query.isEmpty {
items = fruits
} else {
items = fruits.filter { $0.contains(query) }
}
}
}
}
}
struct DetailView: View {
let text: String
var body: some View {
Text(text)
}
}
Gif illustrating the problem:

Picker scroll through one element horizontally

I have a SwiftUI Picker in which an item is selected. The text of one element can be large, so I used UIKit UIPickerView and set the manual height to 100, but at some point it became not enough. Is it possible to make scrolling horizontal for each element?
I want to get something like this:
Picker("Items", select: self._selectItem) {
ForEach(self.items, id: \.self) { item in
ScrollView(.horizontal, showsIndicators: false) {
Text(item.description)
}
.tag(item)
}
}
That should work fine. If you only want to scroll one item, you would have to insert a check of the item length.
let items = [
"A long item text.",
"And a even longer item text which is really going further.",
"Another item text which is really going further."
]
struct ContentView: View {
#State private var select = ""
var body: some View {
VStack {
Text("Make your selection!")
List(items, id: \.self) { item in
ScrollView(.horizontal) {
Text(item)
}
.listRowBackground(item == select ? Color.red : Color.white)
.onTapGesture {
select = item
}
}
}
}
}
I would strongly suggest to separate the picking from the text display and scrolling, e.g. like this:
struct ContentView: View {
#State private var select = items[0]
var body: some View {
VStack {
Text("Make your selection!")
Picker("Items", selection: $select) {
ForEach(items) { item in
Text(item.title)
.tag(item)
}
}
ScrollView {
Text(select.text)
}
.padding()
.frame(height: 200)
}
}
}

With SwiftUI 3.0 .swipeActions in a ForEach how do you have an action go to another view while passing that view the input argument of the ForEach?

I am tying to add a .swipeAction to a ForEach list in which I want to pass the element in the list that was selected by the user to another invoked View. In other words when the user swipes on an item in the list, I want the user to be taken to a new View which has the contents of that item in the list so that it can display details from that item in that new view.
With that said, I have mocked up this simple example which I hope helps show the issue I am having.
import SwiftUI
struct ContentView: View {
var colors : [Color] = [Color.red, Color.green,
Color.blue, Color.yellow,
Color.brown, Color.cyan, Color.pink]
var body: some View {
NavigationView {
VStack {
List {
ForEach(colors, id: \.self) { color in
ColorRowView(color: color)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(action: { print("Hello From The First Button") },
label: {Label("Hello", systemImage: "face.smiling")})
.tint(Color.orange)
NavigationLink(destination: ColorShowView(color: color),
label: {Image(systemName: "magnifyingglass")})
.tint(.yellow)
}
}
}
Spacer()
NavigationLink(destination: ColorShowView(color: Color.red),
label: { Image(systemName: "magnifyingglass" ) } ).tint(.yellow)
}
.navigationViewStyle(StackNavigationViewStyle())
.navigationTitle(Text("List Of Colors"))
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct ColorRowView: View {
var color: Color
var body: some View {
VStack {
Text("Color \(color.description)").foregroundColor(color)
}
}
}
struct ColorShowView: View {
var color: Color
var body: some View {
VStack {
Text("Color Name \(color.description)").foregroundColor(color)
Text("Color Hash Value \(color.hashValue)").foregroundColor(color)
}
}
}
What I find is that if I put a NavigationLink as a button on the .swipeActions it shows up correctly, but when tapped the NavigationLink does not execute and hence does not take you to a new View.
If I move that same Navigation Link down to after the .swipeActions, and invoke it with some #State it works, but it adds another row in-between each row in the list of the ForEach. In other words, the ForEach of course sees it as part of its list and adds it in with the other items in the list. Even if I add a .hidden() onto that NavigationLink, it still takes up space with a new row, it just hides the contents of the row, not the row itself.
If I move the NavigationLink outside of the ForEach, then the input argument of color from the ForEach is out of scope. It will correctly build the view and execute the link (using an action and some #State), but it can not pass the color input from the ForEach because of course it is out of scope. If you hard code a color in its place it works fine, except of course for the fact that it does not have the color from the users selection from the list.
Note I put a simple NavigationLink on the bottom of the view as well just so that I could see that it worked correctly outside of the issue with the .swipeActions, and it does work fine with a hard coded color value like Color.red.
This is of course a very made up example, but I think it does show the issue.
Has anyone used a .swipeActions to invoke a NavigationLink to a new view passing into that view the users selected item (in this case the color)? If so how do you get that to work. It feels like a chicken and the egg problem, I can not seem to both have access to the scope in which the input data (the color) is available, and a NavigationLink that does not become part of the view of the ForEach list.
Anyone have any ideas? Thanks in advance for any and all commentary, corrections, ideas, etc.
First solution: by using fullScreenCover and #State var selectedColor
#Environment(.presentationMode) var presentationMode
// ContentView.swift
// StackOverFlow
//
// Created by Mustafa T Mohammed on 12/31/21.
//
import SwiftUI
struct ContentView: View {
#State var isColorShowViewPresented = false
#State var selectedColor: Color = .yellow
var colors : [Color] = [Color.red, Color.green,
Color.blue, Color.yellow,
Color.brown, Color.cyan, Color.pink]
var body: some View {
NavigationView {
VStack {
// you can use List instead of ForEach loop it's the same
// less code :)
List(colors, id: \.self) { color in
ColorRowView(color: color)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(action: {
isColorShowViewPresented.toggle() // toggle isColorShowViewPresented to trigger the
// fullScreenCover
selectedColor = color
print("Hello From The First Button")
},
label: {Label("Hello", systemImage: "face.smiling")})
.tint(Color.orange)
}
}
Spacer()
.fullScreenCover(isPresented: $isColorRowViewPresented) {
// if you like to implement what happen when user dismiss the presented view
print("user dissmissed ColorRowView")
} content: {
ColorShowView(color: selectedColor) // view that you want to present
}
}
.navigationViewStyle(StackNavigationViewStyle())
.navigationTitle(Text("List Of Colors"))
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct ColorRowView: View {
var color: Color
var body: some View {
VStack {
Text("Color \(color.description)").foregroundColor(color)
}
}
}
struct ColorShowView: View {
#Environment(\.presentationMode) var presentationMode
var color: Color
var body: some View {
VStack {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("Dismiss")
}
Spacer()
Text("Color Name \(color.description)").foregroundColor(color)
Text("Color Hash Value \(color.hashValue)").foregroundColor(color)
Spacer()
}
}
}
Second solution: NavigationLink and #State var selectedColor
//
// ContentView.swift
// StackOverFlow
//
// Created by Mustafa T Mohammed on 12/31/21.
//
import SwiftUI
struct ContentView: View {
#State var isColorShowViewPresented = false
#State var selectedColor: Color = .yellow
var colors : [Color] = [Color.red, Color.green,
Color.blue, Color.yellow,
Color.brown, Color.cyan, Color.pink]
var body: some View {
NavigationView {
VStack {
// you can use List instead of ForEach loop it's the same
// less code :)
List(colors, id: \.self) { color in
ColorRowView(color: color)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
Button(action: {
isColorShowViewPresented.toggle() // toggle isColorShowViewPresented to trigger the
// NavigationLink
selectedColor = color
print("Hello From The First Button")
},
label: {Label("Hello", systemImage: "face.smiling")})
.tint(Color.orange)
}
}
Spacer()
NavigationLink("", isActive: $isColorShowViewPresented) {
ColorShowView(color: selectedColor)
}
}
.navigationViewStyle(StackNavigationViewStyle())
.navigationTitle(Text("List Of Colors"))
.navigationBarTitleDisplayMode(.inline)
}
}
}
struct ColorRowView: View {
var color: Color
var body: some View {
VStack {
Text("Color \(color.description)").foregroundColor(color)
}
}
}
struct ColorShowView: View {
var color: Color
var body: some View {
VStack {
Text("Color Name \(color.description)").foregroundColor(color)
Text("Color Hash Value \(color.hashValue)").foregroundColor(color)
}
}
}

The correct way to list CoreData object in nest NavigationView in NavigationLink

In xcode 12 (swift 5.3), I using the conditional navigationLink navigate to another navigationView to list coreData object with NavigationLink. But it seems the AnotherView's NavigationTitle can not be correctly show at the top of screen, instead it padding to the top. The list in another navigationView have a external white background color. The something.id which I want to pass to SomethingView report Argument passed to call that takes no arguments error, but I can get something.name in Text.
struct StartView: View {
#State var changeToAnotherView: String? = nil
var body: some View {
NavigationView {
VStack(spacing: 20) {
...
NavigationLink(destination: AnotherView(), tag: "AnotherView",
selection: $changeToAnotherView) { EmptyView() }
}
}
}
}
struct AnotherView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Something.entity(), sortDescriptors: []) var somethings: FetchedResults<Something>
...
var body: some View {
NavigationView {
List {
ForEach(self.somethings, id: \.id) { something in
NavigationLink(destination: SomethingView(somethingID: something.id)) {
Text(something.name ?? "unknown name")
}
}
}
.navigationBarTitle("SomethingList")
}
}
}
You don't need second NavigationView - it must be only one in view hierarchy, as well it is better to pass CoreData object by reference (view will be able to observe it), so
struct AnotherView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: Something.entity(), sortDescriptors: []) var somethings: FetchedResults<Something>
...
var body: some View {
List {
ForEach(self.somethings, id: \.id) { something in
NavigationLink(destination: SomethingView(something: something)) {
Text(something.name ?? "unknown name")
}
}
}
.navigationBarTitle("SomethingList")
}
}
struct SomethingView: View {
#ObservedObject var something: Something
var body: some View {
// .. your next code

Corrupted Navigation Views

I'm pretty sure this is a bug in SwiftUI, but I wondered if anyone has encountered it and figured out a workaround. My normal use case is to have a search field appear, but I've simplified it to the point where a simple text string exhibits the bug.
Create a single-view app, copy this into ContentView, and run it. Tap the search icon twice, then scroll the view; you'll see the text scrolling UNDER the title.
import SwiftUI
struct ContentView: View {
private var items = (0 ... 50).map {String($0)}
#State private var condition = false
var searchButton: some View {
Button(action: {self.condition.toggle()}) {
Image(systemName: "magnifyingglass").imageScale(.large)
}
}
var body: some View {
NavigationView {
VStack {
if condition {
Text("Peekaboo")
}
List {
ForEach(items, id: \.self) {item in
HStack {
Text(item)
}
}
}
}
.navigationBarTitle("List of Items")
.navigationBarItems(leading: searchButton)
}
}
}
Maybe it is a bug, submit feedback to Apple, but currently this is how NavigationView behaves - it collapses navigation bar only if its top content is List/ScrollView/Form. So to solve the issue move your VStack either into a List or out of NavigationView
1)
var body: some View {
NavigationView {
List {
if condition {
Text("Peekaboo")
}
ForEach(items, id: \.self) {item in
2)
var body: some View {
VStack {
if condition {
Text("Peekaboo")
}
NavigationView {
List {
It seems that a View cannot cope with variable number of views.
A workaround this strange behavior is this:
import SwiftUI
struct ContentView: View {
private var items = (0 ... 50).map {String($0)}
#State private var condition = false
var searchButton: some View {
Button(action: {self.condition.toggle()}) {
Image(systemName: "magnifyingglass").imageScale(.large)
}
}
var body: some View {
NavigationView {
VStack {
if condition {
Text("Peekaboo")
} else {
Text("")
}
// or use this Text(condition ? "Peekaboo" : "")
List {
ForEach(items, id: \.self) {item in
HStack {
Text(item)
}
}
}
}
.navigationBarTitle("List of Items")
.navigationBarItems(leading: searchButton)
}
}
}
Let me know if it works, if not let us know what device/system you are using. Tested with Xcode 11.6 beta, Mac 10.15.5, target ios 13.5 and mac catalyst.