How to animate view appearance? - swift

I have a custom view. It used as a cell in a List view. I would like to animate appearance of a Group subview on quote.expanded = true (e.g. fading).
.animation(.default) modifier does not work.
struct QuoteView: View {
var quote : QuoteDataModel
var body: some View {
VStack(alignment: .leading, spacing: 5) {
Text(quote.latin)
.font(.title)
if quote.expanded {
Group() {
Divider()
Text(quote.russian).font(.body)
}
}
}
}
}

The following code animates for me. Note that animation inside a list, while still probably better than no animation, can still look kind of weird. This is because the height of the list rows themselves do not animate, and snap to their final height, while the view inside the row does animate. This is a SwiftUI issue, and there's not anything you can do about it for now other than file feedback that this behavior doesn't look great.
struct StackOverflowTests: View {
#State private var array = [QuoteDataModel(), QuoteDataModel(), QuoteDataModel()]
var body: some View {
List {
ForEach(array.indices, id: \.self) { index in
QuoteView(quote: self.array[index])
.onTapGesture { self.array[index].expanded.toggle() }
}
}
}
}
struct QuoteView: View {
var quote : QuoteDataModel
var body: some View {
VStack(alignment: .leading, spacing: 5) {
Text(quote.latin)
.font(.title)
if quote.expanded {
Group() {
Divider()
Text(quote.russian).font(.body)
}
}
}
.animation(.default)
}
}

Use a Transition to animate the view Appearance:
https://developer.apple.com/tutorials/swiftui/animating-views-and-transitions
https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-and-remove-views-with-a-transition

try this....but you will see, that there are still other problems, because the text is left aligned...
var body: some View {
VStack(alignment: .leading, spacing: 5) {
Button("Tap me") {
withAnimation() {
self.expanded.toggle()
if self.expanded {
self.opacity = 1
} else {
self.opacity = 0
}
}
}
Text("aha")
.font(.title)
if expanded {
Group() {
Divider()
Text("oho").font(.body)
}.opacity(opacity)
}
}
}

Related

How to Detect When User Reaches Bottom of the Screen and Trigger a Function in SwiftUI

What is the best way to detect when a user has scrolled to the bottom of the screen and trigger a specific function in SwiftUI?
var body: some View {
NavigationView {
VStack {
ZStack {
Text("Pokedex")
.font(.custom("Pokemon Solid", size: 25))
.foregroundColor(.yellow)
.opacity(0.3)
GeometryReader { geometry in
ScrollView {
LazyVGrid(columns: adaptiveColumns, spacing: 5) {
ForEach(viewModel.filteredPokemon) { pokemon in
NavigationLink(destination: PokemonDetailView(pokemon: pokemon)
) {
PokemonView(vm: viewModel, pokemon: pokemon)
}
}
}
.animation(.easeInOut(duration: 0.3), value: viewModel.filteredPokemon.count)
.navigationTitle("Pokedexarino")
.navigationBarTitleDisplayMode(.inline)
if geometry.frame(in: .global).maxY == geometry.safeAreaInsets.bottom {
Text("Reached the bottom")
//TODO: make load 30 at a time
}
}
}
.searchable(text: $viewModel.searchText)
}
}
}
.environmentObject(viewModel)
}
I tried to create a function but I think there may be a built in function that can do this. I tried Geometry reader.
You can add paging to your ScrollView by displaying an additional row at the bottom if more data is available, then using the .task modifier to load the next page, e.g.
ScrollView {
LazyVGrid(columns: adaptiveColumns, spacing: 5) {
ForEach(viewModel.filteredPokemon) { pokemon in
NavigationLink(destination: PokemonDetailView(pokemon: pokemon)
) {
PokemonView(vm: viewModel, pokemon: pokemon)
}
}
if viewModel.moreDataAvailable {
Text("Reached the bottom")
.task {
await viewModel.loadNextPage()
}
}
}
}

Navigation stack yellow warning triangle

I'm attempting to listen for a change in a boolean value & changing the view once it has been heard which it does successfully, however, results in a yellow triangle. I haven't managed to pinpoint the issue but it doesn't seem to have anything to do with the view that it's transitioning to as even when changed the error still persists.
My code is below
import SwiftUI
struct ConversationsView: View {
#State var isShowingNewMessageView = false
#State var showChat = false
#State var root = [Root]()
var body: some View {
NavigationStack(path: $root) {
ZStack(alignment: .bottomTrailing) {
ScrollView {
LazyVStack {
ForEach(0..<20) { _ in
Text("Test")
}
}
}.padding()
}
Button {
self.isShowingNewMessageView.toggle()
} label: {
Image(systemName: "plus.message.fill")
.resizable()
.renderingMode(.template)
.frame(width: 48, height: 48)
.padding()
.foregroundColor(Color.blue)
.sheet(isPresented: $isShowingNewMessageView, content: {
NewMessageView(show: $isShowingNewMessageView, startChat: $showChat)
})
}
}
.onChange(of: showChat) { newValue in
guard newValue else {return}
root.append(.profile)
}.navigationDestination(for: Root.self) { navigation in
switch navigation {
case .profile:
ChatView()
}
}
}
enum Root {
case profile
}
}
ChatView() Code:
import SwiftUI
struct ChatView: View {
#State var messageText: String = ""
var body: some View {
VStack {
ScrollView {
VStack(alignment: .leading, spacing: 12) {
ForEach(MOCK_MESSAGES) { message in
MessageView(message: message)
}
}
}.padding(.top)
MessageInputView(messageText: $messageText)
.padding()
}
}
}
Any support is much appreciated.
You should use navigationDestination modifier inside your NavigationStack component, just move it.
NavigationStack(path: $root) {
ZStack(alignment: .bottomTrailing) {
ScrollView {
LazyVStack {
ForEach(0..<20) { _ in
Text("Test")
}
}
}.padding()
}.navigationDestination(for: Root.self) { navigation in
switch navigation {
case .profile:
ChatView()
}
}
//...
}
Basically this yellow triangle means NavigationStack can't find suitable component for path. And when you using navigationDestination directly on NavigationStack View or somewhere outside it is ignored
You must set .environmentObject(root) to NavigationStack in order to provide the NavigationPath to the view subhierarchy (ChatView in your case). Also you must have a #EnvironmentObject property of type Root in your ChatView so that it can read the path.

ZStack blocks animation SwiftUI

So my goal is to be able to show a custom view from time to time over a SwiftUI tabview, so I thought I would place them both in a ZStack like this
#State var show = true
#State private var selectedTab : Int = 0
var body: some View {
ZStack {
TabView(selection: $selectedTab) {
Color.pink
}
if show {
Button(action: {
withAnimation(Animation.linear(duration: 10)) {
show = false
}
}) {
Color.blue
}
.frame(width: 100, height: 100)
}
}
}
This works just fine, but when I try to use withAnimation() no animation gets triggered. How can I make the overlaying view, disappear with animation?
Use .animation modifier with container, like below, so container could animate removing view
ZStack {
TabView(selection: $selectedTab) {
Color.pink
}
if show {
Button(action: {
show = false // << withAnimation not needed anymore
}) {
Color.blue
}
.frame(width: 100, height: 100)
}
}
.animation(Animation.linear(duration: 10), value: show) // << here !!
So I found a solution and what I think is the cause of this. My hypothesis is that the animation modifier does not handle ZIndex IF it is not explicitly set.
One solution to this is to set ZIndex to the view that should be on the top to something higher than the other view. Like this:
#State var show = true
#State private var selectedTab : Int = 0
var body: some View {
ZStack {
TabView(selection: $selectedTab) {
Color.pink
}
if show {
Button(action: {
withAnimation {
show = false
}
}) {
Color.blue
}
.frame(width: 100, height: 100)
.zIndex(.infinity) // <-- this here makes the animation work
}
}
}

How to use .firstTextBaseline alignment with a view that contains GeometryReader

I have two views in a HStack - the left one is a simple Text view.
For the right view, i'm experimenting with using GeometryReader. I cannot get the baselines of the text aligned when I use geometry reader, but I can when I don't use it.
Here is some code:
struct CompositeView : View {
var body : some View {
VStack {
Button(action: {
}) {
Text("Another text")
.padding(5)
.overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.blue, lineWidth: 2.0))
}
}
}
}
struct GeometryReaderTest : View {
var tags: [String]
init(tags : [String]) {
self.tags = tags
}
#State private var viewHeight = CGFloat.zero
var body: some View {
VStack {
GeometryReader { geometry in
ZStack {
ForEach(self.tags, id: \.self) { tag in
Button(action: {}) {
Text(tag)
.padding(5)
.overlay(RoundedRectangle(cornerRadius: 5).stroke(Color.blue, lineWidth: 2.0))
}
}
}
.background(getHeight(self.$viewHeight))
}
}
.frame(height: viewHeight)
}
/// Get the height of the Geometry view
///
private func getHeight(_ height: Binding<CGFloat>) -> some View {
return GeometryReader { geometry -> Color in
let rect = geometry.frame(in: .local)
DispatchQueue.main.async {
height.wrappedValue = rect.size.height
}
return .clear
}
}
}
struct MyTestView : View {
var body : some View {
VStack {
HStack (alignment: .firstTextBaseline) {
ZStack {
Text("Hello")
}
GeometryReaderTest(tags: ["One"])
}
HStack (alignment: .firstTextBaseline) {
ZStack {
Text("Hello")
}
CompositeView()
}
}
}
}
Here is the outcome:
As you can see, the baseline alignment doesn't work when the right hand view is a GeometryReader, but it does work when the right hand view is a simple button.
Does anyone know how I can get the baselines to line up?
I'm using GeometryReader because my right-view will eventually be more complex - I'm reducing the issue I'm facing to as small a repro as possible.
Thank you!

How to animate navigationBarHidden in SwiftUI?

struct ContentView: View {
#State var hideNavigationBar: Bool = false
var body: some View {
NavigationView {
ScrollView {
VStack {
Rectangle().fill(Color.red).frame(height: 50)
.onTapGesture(count: 1, perform: {
withAnimation {
self.hideNavigationBar.toggle()
}
})
VStack {
ForEach(1..<50) { index in
HStack {
Text("Sample Text")
Spacer()
}
}
}
}
}
.navigationBarTitle("Browse")
.navigationBarHidden(hideNavigationBar)
}
}
}
When you tap the red rectangle it snaps the navigation bar away. I thought withAnimation{} would fix this, but it doesn't. In UIKit you would do something like this navigationController?.setNavigationBarHidden(true, animated: true).
Tested in xCode 12 beta 6 and xCode 11.7
You could try using
.navigationBarHidden(hideNavigationBar).animation(.linear(duration: 0.5)) instead of .navigationBarHidden(hideNavigationBar)
and also move self.hideNavigationBar.toggle() out of the animation block. That is not required if you use the above approach for hiding of navigation bar with animation.
I think, the only solution is to use a position function in SwiftUI 2
var body: some View {
GeometryReader { geometry in
NavigationView {
ZStack {
Color("background")
.ignoresSafeArea()
// ContentView
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading: logo, trailing: barButtonItems)
.toolbar {
ToolbarItem(placement: .principal) {
SearchBarButton(placeholder: LocalizedStringKey("home_vc.search_bar.placeholder"))
.opacity(isNavigationBarHidden ? 0 : 1)
.animation(.easeInOut(duration: data.duration))
}
}
}
.frame(height: geometry.size.height + (isNavigationBarHidden ? 70 : 0))
// This is the key ⬇
.position(x: geometry.size.width/2, y: geometry.size.height/2 - (isNavigationBarHidden ? 35 : 0))
.animation(.easeInOut(duration: 0.38))
.onTapGesture {
isNavigationBarHidden.toggle()
}
}
}
I'm still learning animation in SwiftUI but at this stage, I understand that you must animate the parent view.
So your code would become...
struct ContentView: View {
#State var hideNavigationBar: Bool = false
var body: some View {
NavigationView {
ScrollView {
VStack {
Rectangle().fill(Color.red).frame(height: 50)
.onTapGesture(count: 1) {
self.hideNavigationBar.toggle()
}
VStack {
ForEach(1..<50) { index in
HStack {
Text("Sample Text")
Spacer()
}
}
}
}
}
.navigationBarTitle("Browse")
.navigationBarHidden(hideNavigationBar)
.animation(.spring()) // for example
}
}
}
Note that the last argument in any function call can be placed into a single closure.
So...
.onTapGesture(count: 1, perform: {
self.hideNavigationBar.toggle()
})
can become...
.onTapGesture(count: 1) {
self.hideNavigationBar.toggle()
}
Simpler syntax in my humble opinion.