SwiftUI animation transition of conditional views - swift

So, i have this little app with a button where i can change the how the main view display its items, with a grid or list.
But i would like to add a little animation when it makes its transition from one to another. I messed i little bit trying to add withAnimationon the button, or .transition() inside the views, but nothing seems to do the trick.
Any tips on how can i achieve this?
struct FrameworkGridView: View {
#StateObject var viewModel = FrameworkGridViewModel()
#Binding var isGrid: Bool
var body: some View {
NavigationView {
Group {
if viewModel.isGrid {
ScrollView {
LazyVGrid(columns: viewModel.columns) {
ForEach(MockData.frameworks) { framework in
FrameworkTitleView(framework: framework, isGrid: $viewModel.isGrid)
.onTapGesture {
viewModel.selectedFramework = framework
}
}
}
}
.sheet(isPresented: $viewModel.isShowingDetailView) {
DetailView(framework: viewModel.selectedFramework ?? MockData.sampleFramework, isShowingDetailView: $viewModel.isShowingDetailView, isGrid: $viewModel.isGrid)
}
} else {
List {
ForEach(MockData.frameworks) { framework in
NavigationLink(destination: DetailView(framework: framework, isShowingDetailView: $viewModel.isShowingDetailView, isGrid: $viewModel.isGrid)) {
FrameworkTitleView(framework: framework, isGrid: $viewModel.isGrid)
}
}
}
}
}
.toolbar {
Button {
viewModel.isGrid.toggle()
} label: {
Image(systemName: viewModel.isGrid ? "list.dash" : "square.grid.2x2")
}
}
}
}
}
[]

I believe you're looking for matchedGeometryEffect. Check out WWDC 2020: What's New in SwiftUI starting around 19 minutes in. The SwiftUI Lab also has a couple of articles about it.

Related

Preselecting a (dynamic) item in NavigationSplitView

I'm using NavigationSplitView to structure the user interface of my (macOS) app like this:
struct NavigationView: View {
#State private var selectedApplication: Application?
var body: some View {
NavigationSplitView {
ApplicationsView(selectedApplication: $selectedApplication)
} detail: {
Text(selectedApplication?.name ?? "Nothing selected")
}
}
}
The sidebar is implemented using ApplicationsView that looks like this:
struct ApplicationsView: View {
#FetchRequest(fetchRequest: Application.all()) private var applications
#Binding var selectedApplication: Application?
var body: some View {
List(applications, selection: $selectedApplication) { application in
NavigationLink(value: application) {
Text(application.name)
}
}
// This works, but looks a bit complicated and... ugly
.onReceive(applications.publisher) { _ in
DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
if selectedApplication == nil {
selectedApplication = applications.first
}
}
}
// This also does not work, as the data is not yet available
.onAppear {
selectedApplication = applications.first
}
}
}
I'm currently preselecting the first Application item (if it exists) using the shown onReceive code, but it looks complicated and a bit ugly. For example, it only works properly when delaying the selection code.
Is there a better way to achieve this?
Thanks.
How about just setting the selectedApplication using the task modifier with an id as follows:
struct ApplicationsView: View {
#FetchRequest(fetchRequest: Application.all()) private var applications
#Binding var selectedApplication: Application?
var body: some View {
List(applications, selection: $selectedApplication) { application in
NavigationLink(value: application) {
Text(application.name!)
}
}
.task(id: applications.first) {
selectedApplication = applications.first
}
}
}
the task is fired when the view is first displayed, and when the id object is updated, so this works without introducing a delay

How to animate hideable views with SwiftUI?

I'm trying out SwiftUI, and while I've found many of its features very elegant, I've had trouble with animations and transitions. Currently, I have something like
if shouldShowText { Text(str).animation(.default).transition(AnyTransition.opacity.animation(.easeInOut)) }
This label does transition properly, but when it's supposed to move (when another view above is hidden, for instance) it does not animate as I would have expected, but rather jumps into place. I've noticed that wrapping everything in an HStack works, but I don't see why that's necessary, and I was hoping that there is a better solution out there.
Thanks
If I correctly understood and reconstructed your scenario you need to use explicit withAnimation (depending on the needs either for "above view" or for both) as shown below
struct SimpleTest: View {
#State var shouldShowText = false
#State var shouldShowAbove = true
var body: some View {
VStack {
HStack
{
Button("ShowTested") { withAnimation { self.shouldShowText.toggle() } }
Button("HideAbove") { withAnimation { self.shouldShowAbove.toggle() } }
}
Divider()
if shouldShowAbove {
Text("Just some above text").padding()
}
if shouldShowText {
Text("Tested Text").animation(.default).transition(AnyTransition.opacity.animation(.easeInOut))
}
}
}
}

How to access content view's elements later in SwiftUI?

Let's say that I have a content view like this one:
struct ContentView: View {
#State private var selection = 0
var body: some View {
TabView(selection: $selection) {
CustomClass()
.tabItem {
VStack {
Image("First")
Text("First")
}
}
.tag(0)
Button(action: { EmployeeStorage.sharedInstance.reload() }) {
Text("Reload")
}
.tabItem {
VStack {
Image("Second")
Text("Second")
}
}
.tag(1)
}
}
}
// MARK: - SomeDelegateThatUpdatesMeLater
extension ContentView: SomeDelegateThatUpdatesMeLater {
func callback() {
// Here I want to update my content view's subviews
// The ContentClass instance needs to be updated
}
}
Let's say that I want to listen to a callback method and then update the content view's tab number 1 (CustomClass) later on. How to access the content view's subviews? I'd need something like UIKit's subviewWithTag(_:). Is there any equivalent in SwiftUI?
You should probably rethink your approach. What exactly is it you want to happen? You have some external data model type that fetches (or updates) data and you want your views to react to that? If that's the case, create an ObservableObject and pass that to your CustomClass.
struct CustomClass: View {
#ObservedObject var model: Model
var body: some View {
// base your view on your observed-object
}
}
Perhaps you want to be notified of events that originate from CustomClass?
struct CustomClass: View {
var onButtonPress: () -> Void = { }
var body: some View {
Button("Press me") { self.onButtonPress() }
}
}
struct ParentView: View {
var body: some View {
CustomClass(onButtonPress: { /* react to the press here */ })
}
}
Lastly, if you truly want some kind of tag on your views, you can leverage the Preferences system in SwiftUI. This is a more complicated topic so I will just point out what I have found to be a great resource here:
https://swiftui-lab.com/communicating-with-the-view-tree-part-1/

What is the correct way to update ScrollView in SwiftUI? (MacOS App)

I am using officially released Xcode 11 from appstore.
A number of things can be seen if you run the code below.
Tapping on Root Press correctly adds the missing view#2, But there is no animation. Why is there no animation?
Tapping any of the Press buttons, correctly adds a middle view, but if you look, you will see that the scrollView content size did not update and therefore the content is clipped. What is the correct way to update the ScrollView?
import SwiftUI
struct ContentView: View {
#State var isPressed = false
var body: some View {
VStack {
Button("Root Press") { withAnimation { self.isPressed.toggle() } }
ScrollView {
Group {
SampleView(index: 1)
if isPressed { SampleView(index: 2) }
SampleView(index: 3)
SampleView(index: 4)
}
.border(Color.red)
}
}
}
}
struct SampleView: View {
#State var index: Int
#State var isPressed = false
var body: some View {
HStack {
VStack {
Text("********************")
Text("This View = \(index)")
Text("********************")
if isPressed {
Text("********************")
Text("-----> = \(index)")
Text("********************")
}
}
Button("Press") { withAnimation { self.isPressed.toggle() } }
}
}
}
Fixing the animations can be done via: .animation(.linear(duration: 0.3)). You can then remove all the animation blocks. (withAnimation { }). As for the bounds/frame, setting the frame helps (when adding the row at the root level), but it doesn't seem to work when you are dealing with an inner view. I added the following: .frame(width:UIScreen.main.bounds.width), and it will look like the following:

SwiftUI: Animate Cells within a Form

I am trying to animate my Form or rather the cells within it. My problem is that the following code give me a nice insertion animation but for the removal the cell is suddenly removed after am ugly looking delay.
import SwiftUI
struct ContentView: View {
#State var toggledValue = false
#State var pickedValue = 0
var body: some View {
NavigationView {
Form {
Section {
Toggle(isOn: $toggledValue) {
Text("Toggled Value")
}
if toggledValue {
Picker(selection: $pickedValue, label: Text("Picked Value")) {
ForEach((0...5).identified(by: \.self)) {
Text("Pick Value \($0)").tag($0)
}
}
}
}
Section {
Text("Some Text")
}
}
.navigationBarTitle("Navigation Bar Title")
}
}
}
What I tried so far is to to wrap the Toggle in a withAnimation closure but this does not change anything. What makes me wondering is that the same code using List instead of Form gives me the expected Animation. Is that a bug or am I overseeing something?
This will probably work (tested in iOS 16 in a similar situation):
Add #State private var isShowingPicker = false
Replace if toggledValue by if isShowingPicker
Under .navigationBarTitle(...) add:
onChange(of: toggledValue)
{ newValue in
withAnimation { isShowingPicker = newValue }
}