How did Apple create the navigation view in this modal? - swiftui

In iOS Settings > Music > Apple Music and Privacy, there is a modal that I've mostly successfully duplicated except in two ways (a brief demo of Apple's modal is here). First, I don't know how to duplicate the bullet points seen in the beginning of the text. Second, I don't know how to hide the nav bar title until scrolling past the logo and modal title. That behavior matches the .inline nav title style and suggests the logo and title are actually part of the nav bar title.
The answer here was really helpful in actually getting the nav bar to show up. I also tried using ToolbarItem and .principal from here to add an image to the toolbar, but that doesn't give the desired result. I tried adding padding because the image was squished into the nav bar but that didn't work. Lastly, I tried a VStack in the .principal placement to add the image and text below it, but that didn't work either.
Here's the code in the parent view:
struct ModalNavBar: View {
#State private var showApplePolicy = false
var body: some View {
Button() {
showApplePolicy = true
} label: {
Text("Apple Music and Privacy")
}.sheet(isPresented: $showApplePolicy, content: {
NavigationView {
TermsAndPrivacyView()
.navigationTitle("Apple Music & Privacy")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
VStack {
Image("privacy")
.resizable()
.frame(width: 80, height: 65)
.padding()
.padding(.vertical, 20)
Text("Apple Music & Privacy")
}
}
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
self.showApplePolicy = false
}) {
Text("Done").bold()
}
}
}
}
})
}
}
and the modal view:
struct TermsAndPrivacyView: View {
var body: some View {
ScrollView {
VStack(alignment: .center, spacing: 20) {
Group {
Image("privacy")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 80, height: 65)
.padding()
.padding(.top, 20)
.padding(.bottom, 5)
Text("Apple Music & Privacy")
.font(.title).bold().padding(.top, -20)
VStack(alignment: .center) {
Text("Apple Music is designed to protect your information and enable you to choose what you share.")
}
}
}.padding(.horizontal, 30)
}
}
}
Any help with the nav bar would be appreciated.

Firstly, This question should have been broken up into multiple questions. Normally that would have gotten the question closed, but here people only downvoted this. Make sure to read the links that I put in my first comment for future questions. Your questions are:
How do I put place two texts or images next to each other and align them?
How do I hide and show the .navigationTitle when a certain view is fully hidden behind the navigation bar?
In answer to the first question, I used a mix of .firstTextBaseline alignment and an .alignmentGuide to put the text bullet in the correct spot. While I hardcoded these values, try to avoid it if things can change. In this case, the view is unchanging so it is acceptable.
As to the hiding and showing the title, I used a PreferenceKey to read the location of the bottom of the Text("Apple Music & Privacy") view in the ScrollView. When that hits zero, this view is behind the navigation bar. Then I simply set a #State variable to toggle when this happens, and use that to set the title.
You will also notice I pulled everything that was in your ModalNavBar out. In a case like this, everything should be contained in the view that the sheet is displaying. It works better, and encapsulates things so you could just drop a different view into the sheet and it still works fine.
Lastly, I have omitted a bunch of text views to keep the answer brief. Feel free to add them back in. When you see the ellipsis, that simply means omitted code.
struct ModalNavBar: View {
#State private var showApplePolicy = false
var body: some View {
Button() {
showApplePolicy = true
} label: {
Text("Apple Music & Privacy")
}.sheet(isPresented: $showApplePolicy, content: {
NavigationView {
TermsAndPrivacyView()
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
self.showApplePolicy = false
}) {
Text("Done").bold()
}
}
}
}
})
}
}
struct TermsAndPrivacyView: View {
#State var showTitle = false
var body: some View {
ScrollView {
VStack(alignment: .center, spacing: 20) {
Image(systemName: "person.3.fill")
.font(.system(size: 80))
Text("Apple Music & Privacy")
.font(.title).bold()
// The PreferenceKey reads where it is within the ScrollView. "Scroll" is a coordinate space name
// set on the ScrollView. .maxY is simply the value of the Y dimension at the bottom of that view
// within the coordinates of the ScrollView. When that hits zero, it is behind the navigation bar.
.background(
GeometryReader {
Color.clear.preference(key: TextLocationPrefKey.self,
value: $0.frame(in: .named("Scroll")).maxY)
}
)
Text("Apple Music is designed to protect your information and enable you to choose what you share.")
// Put the text bullet and text together in an HStack with a .firstTextBaseline alignement
HStack(alignment: .firstTextBaseline) {
Text(Image(systemName: "circle.fill"))
.font(.system(size: 4))
.opacity(0.5)
// The .alignmentGuide let's you tweak the actual alignment. In this case I moved it up by 2.
.alignmentGuide(.firstTextBaseline) { context in
context.height + 2
}
Text("Lorem ipsum dolor sit amet, praesent necessitatibus ei has, te sit hinc munere, ea vix fugit novum noluisse. Nulla graeci delicatissimi qui cu, nec doming iudicabit ex, indoctum partiendo an has. In qui sensibus dissentiunt, inani iudico accusamus ei eum. Suas vidit primis vel ad, meis ignota postea at pri, ei usu consul evertitur. Per vocent sadipscing et. Has debitis deterruisset ei, democritum scribentur te duo, purto accommodare id ius.")
}
...
}.padding(.horizontal, 30)
}
// This is a simple ternary condition. If showTitle is true the title is set to "Apple Music & Privacy" else ""
.navigationTitle(showTitle ? "Apple Music & Privacy" : "")
.navigationBarTitleDisplayMode(.inline)
// This is where the PreferenceKey value is compared to zero to set the showTitle variable.
.onPreferenceChange(TextLocationPrefKey.self) { textLocation in
withAnimation(.easeIn(duration: 0.15)) {
showTitle = !(textLocation > 0)
}
}
// This is where you select and name the view for the coordinate space
.coordinateSpace(name: "Scroll")
}
}
// This is how the preference key is set up.
fileprivate struct TextLocationPrefKey: PreferenceKey {
typealias Value = CGFloat
static var defaultValue = CGFloat.zero
static func reduce(value: inout Value, nextValue: () -> Value) {
// This adds or subtracts based on the change in the value as you scroll the view towards the navigation
// bar, nextValue() is negative, reducing value towards zero.
value += nextValue()
}
}

Related

SWIFTUI: app buttons logic - change label in specific scenario

My app has a number of mainButton created by a single struct. The data is passed to these buttons from an array. Then I have a Submit button.
I am trying to have the Submit button to change label to a arrow up icon only when: a given instance of mainButton has been pressed once already and changed its color and size already (this works), and Submit was pressed once already. Now the Submit button's icon changes to a refresh symbol (arrow up), until a diffrent instance of mainButton has been pressed (and this new button in turn has changed the color and size already as expected).
Basically a button representing a value is selected(tapped) and then its content submitted by pressing the Submit button, and if you submit the same button again the submitter is a refresher (arrow up icon) rather than a submitter (Submit label) as you have submitted that given query before; hence if you want to submit the same value again, the button is submit button is labeled as a refresher. The code below should make this clearer.
I am new to Swift/SwiftUI and trying to do this in a simple way as I want to understand it fully. Hence, if possible, I would rather use if conditions and variables rather than advanced methods.
Here is a simplified version of my code with only place holders in it:
//
// AddView.swift
// WriteOn
//
// Created by Maurizio Zappettini on 2/11/23.
//
import SwiftUI
struct AddView: View {
#State private var typeofMessageGeneric: String = ""
#State private var lastButtonTapped: String = ""
#State private var buttonCount: Int = 0
let buttonData = [
(mainButtonText: "A", mainButtonValue: "a"),
(mainButtonText: "B", mainButtonValue: "b"),
(mainButtonText: "C", mainButtonValue: "c"),
(mainButtonText: "D", mainButtonValue: "d"),
]
var body: some View {
ZStack { // Using ZStack for background gradient
Rectangle()
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading)
.foregroundColor(.clear)
.background(LinearGradient(colors: [.blue, .purple], startPoint: .top, endPoint: .bottom))
VStack { //Master VStack
VStack { //Headline
Text("Test View")
.font(.headline)
.padding()
}
GeometryReader { geometry in // Debug alignment
VStack(alignment: .center) { //TextBox
Rectangle()
.fill(Color.cyan)
.cornerRadius(20)
.frame(width: geometry.size.width * 0.8, height: 250, alignment: .center)
.overlay(
HStack(alignment: .top){
VStack {
Spacer()
}
ScrollView {
VStack(alignment: .center) {
Text("getResponsePlaceHolder")
.foregroundColor(.black)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
.layoutPriority(2)
}
}
VStack{
ShareLink(item: "nothing"){
Image(systemName: "square.and.arrow.up")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
.foregroundColor(.black)
}
}
VStack{
Spacer()
}
}
.frame(height: geometry.size.height * 0.6) // find way to make it refer to actual box and not whole screen
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
} //Vstack Top
} //Geo Reader
Spacer(minLength: 50
)
ScrollView{
VStack { //Buttons
ForEach(buttonData, id: \.mainButtonValue) { data in
mainButton(typeofMessageGeneric: self.$typeofMessageGeneric, lastButtonTapped: self.$lastButtonTapped, buttonCount: self.$buttonCount, mainButtonText: data.mainButtonText, mainButtonValue: data.mainButtonValue)
}
} //Button Vstack end
}
VStack{ //Footer
submitButton()
}
}
}
}
func submitButton() -> some View {
return Button(action: getResponse) {
if self.typeofMessageGeneric == self.lastButtonTapped && self.buttonCount == 0 {
Text("Submit")
.font(.system(size: 20))
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(10)
} else {
Image(systemName: "arrow.clockwise.circle")
.font(.system(size: 30))
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(10)
}
}
}
// Create main buttons
struct mainButton: View {
#Binding var typeofMessageGeneric: String // Bind the variable inside this struct with the same one inside the ContentView struct
#Binding var lastButtonTapped: String // Not currently used -- implement later if needed
#Binding var buttonCount:Int
var mainButtonText:String
var mainButtonValue:String
var body: some View {
Button(action: {
self.typeofMessageGeneric = mainButtonValue
self.lastButtonTapped = mainButtonValue
buttonCount = 0
if self.typeofMessageGeneric == mainButtonValue{
buttonCount = 1
} else {
buttonCount = 0
}
}) {
Text(mainButtonText)
Text(String(buttonCount)) // testing only
}
.font(.system(size: 20))
.foregroundColor(.white)
.padding()
.background(self.mainButtonValue == self.typeofMessageGeneric ? Color.teal : Color.purple)
.cornerRadius(10)
.scaleEffect(self.mainButtonValue == self.typeofMessageGeneric ? 1.12 : 1)
.animation(.easeOut, value: 2)
}
}
func getResponse() {
}
}
struct AddView_Previews: PreviewProvider {
static var previews: some View {
AddView()
}
}
EDIT:
Here is a better explanation of what the app does and what I am trying to achieve in terms of button logic.
What's on the screen when you start the app:
There is an empty text box.
There are 4 buttons (it will be more)
There is a Submit button at the bottom
What the app does:
The 4 main buttons are 4 different prompts. Each prompt/button is composed of a label and an associated value.
Tapping a button acts as a way to select that button's corresponding value.
Tapping the submit button will submit the value of the last selected main buttons to an external API. The latter will return some specific data that is visualized in the text box above (this part I left out as it is unrelated to my button label problem).
If the same prompt is submitted again the API will return different data (as there is a randomization mechanism). Hence, the user may want to submit again the same prompt they have submitted already.
How the buttons behave (practical example):
1- You tap button B
2- Button B changes the background color from purple to teal and becomes bigger.
3- You tap the "Submit" button.
4- The submit button changes its label from "Submit" to ARROW_UP.
5- The app submits value "b" to the API and this returns DATA_b_1.
6- DATA_b_1 is visualized in the text box.
7- Button B is still 'selected' (it is still teal and larger)
8- You tap ARROW_UP.
9- Value "b" is submitted again to the API and this now returns
DATA_b_2 (a different random instance of DATA_b).
10- DATA_b_2 is visualized in the text box.
11- You now tap button D.
12- Button D changes the background color from purple to teal and becomes bigger.
13- Button B reverts its color to purple and its size to smaller.
14- ARROW_UP button reverts its label to "Submit".
15- Back to point 3
Everything in the list above works as expected except the "Submit" to ARROW_UP and vice-versa label changes.
I hope I understood what you are trying to achieve, the. question is not very clear to me. What I got is:
Show the question and 4 possible answers to the user. Let the user select an answer. Submit button: disabled.
The user selects one answer; they can change their mind as much as they want until the answer is submitted. Submit button: enabled.
The user pressed "Submit": they can't press any other button, except for "Refresh". Submit button: "arrow up".
The user pressed "Refresh": go back to 1.
If that's the case, you just need to track two things:
what is the selected answer (when none is selected, nothing can be submitted)
when the answer was submitted - then, the only option is to refresh the question
By the way, your variable buttonCount is pointless: if you have only one variable for the whole view, all buttons will show the same number.
With a good set of ifs and elses, the "Submit" button can be disabled, enabled or show "arrow up", and perform the right action. Here's the code:
// Track which button was pressed - or none of them (starting case)
#State private var buttonPressed: String = ""
// Track if a choice was submitted - when submitted, the Submit button can only refresh
#State private var isWaitingToRefresh = false
// For testing only - tests the refresh
#State private var question = "0"
// (other code)
var body: some View {
// (other code)
Text("getResponsePlaceHolder \(question)")
// (other code)
ScrollView{
VStack { //Buttons
ForEach(buttonData, id: \.mainButtonValue) { data in
// Call a function, no need for binding vars
mainButton(mainButtonText: data.mainButtonText,
mainButtonValue: data.mainButtonValue)
}
} //Button Vstack end
}
submitButton()
}
}
}
func submitButton() -> some View {
Button {
// No action if no button was pressed
guard !buttonPressed.isEmpty else { return }
// Check if you need to refresh the question
if isWaitingToRefresh {
// If the view is waiting to refresh, then refresh the question
refresh()
isWaitingToRefresh = false
} else {
// If the view is not waiting to refresh, submit the user's choice
isWaitingToRefresh = true
getResponse()
}
} label: {
if !isWaitingToRefresh {
Text("Submit")
.font(.system(size: 20))
.foregroundColor(.white)
.padding()
// Gray-out the button if the user made no choice yet
.background(buttonPressed.isEmpty ? Color.secondary : Color.blue)
.cornerRadius(10)
} else {
Image(systemName: "arrow.clockwise.circle")
.font(.system(size: 30))
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(10)
}
}
// Disable the button if the user made no choice yet
.disabled(buttonPressed.isEmpty)
}
// Create main buttons
// Call a function, no need for binding vars:
// you can use the same variables of the view
func mainButton(mainButtonText: String,
mainButtonValue: String) -> some View {
Button {
// You just need to change the choice when a button is pressed,
// let the Submit button do the rest
buttonPressed = mainButtonValue
} label: {
Text(mainButtonText)
Text(String("same number for all buttons")) // testing only
}
.font(.system(size: 20))
.foregroundColor(.white)
.padding()
.background(mainButtonValue == buttonPressed ? Color.teal : Color.purple)
.cornerRadius(10)
.scaleEffect(mainButtonValue == buttonPressed ? 1.12 : 1)
.animation(.easeOut, value: 2)
// Do not let the user change the choice if it was already submitted
.disabled(isWaitingToRefresh)
}
// Remember to reset the user's choice (buttonPressed = "")
func refresh() {
buttonPressed = ""
question = String(Int.random(in: 1...100))
}

SwiftUI bottom sheet like note app's format tab

Currently, I have to implement bottom sheet. And I found the very example of my need.
Is this component system component of swift or swiftui?
OR do I need to implement on my own?
PLEASE LET ME KNOW IF U HAVE SOME INFOS! XD
At first I implement with ZStack, drag gesture but the animation is not what I expected.
I need Information about whether there is component like .sheet(isPresented: Bool, content: View) of the modal like above image.
As our friend said before, it is a sheet. Inside the sheet you can either define a new view or call any of your views. Then you have to use the modifier .presentationDetents which receive a Set of PresentationDetents to say where the view has to stop when appearing on the screen. This modifier must be apply to the content of the sheet and not directly to the sheet.
struct ContentView: View {
#State var isSheetShown = false
var body: some View {
VStack {
Button("Show view"){
isSheetShown = true
}
}.sheet(isPresented: $isSheetShown, content: {
StackOfButtons()
.presentationDetents([.medium])
})
.padding()
}
}
Finally, to create that stack type of buttons you can put them all in a HStack, give them individually some padding, set a little of spacing in the HStack and the round the corners of the stack. Something like this:
struct StackOfButtons: View {
var body: some View {
HStack(spacing: 2){
Button {
print("Hola que ase")
} label: {
Image(systemName: "list.bullet")
.padding()
.background(.thinMaterial)
.foregroundColor(.black)
}
Button {
print("Hola que ase")
} label: {
Image(systemName: "list.dash")
.padding()
.background(.thinMaterial)
.foregroundColor(.black)
}
Button {
print("Hola que ase")
} label: {
Image(systemName: "list.number")
.padding()
.background(.thinMaterial)
.foregroundColor(.black)
}
}.cornerRadius(10)
}
}
Result

Navigationlink question in Xcode 13.1 -- Blanks instead of links?

Sorry for the newbie question, but I'm stuck on why Navigationlink produces no links at all. Xcode compiles, but there's a blank for where the links to the new views are. This particular view is View 3 from ContentView, so the structure is ContentView -> View 2 -> View 3 (trying to link to View 4).
struct MidnightView: View {
var hourItem: HoursItems
#State var showPreferencesView = false
#State var chosenVersion: Int = 0
#State var isPsalmsExpanded: Bool = false
#State var showLXX: Bool = false
var body: some View {
ScrollView (.vertical) {
VStack (alignment: .center) {
Group {
Text (hourItem.hourDescription)
.font(.headline)
Text ("Introduction to the \(hourItem.hourName)")
.font(.headline)
.bold()
.frame(maxWidth: .infinity, alignment: .center)
ForEach (tenthenou, id: \.self) {
Text ("\($0)")
Text ("\(doxasi)")
.italic()
}
}
.padding()
NavigationView {
List {
ForEach (midnightHours, id:\.id) {watch in
NavigationLink ("The \(watch.watchName)", destination: MidnightWatchView (midnightItem: watch, chosenVersion: self.chosenVersion, isPsalmsExpanded: self.isPsalmsExpanded, showLXX: self.showLXX))
}
}
}
Group {
Text ("Absolution of the \(hourItem.hourName)")
.font(.headline)
Text (absolutionTexts[(hourItem.hourName)] ?? " ")
Divider()
Text ("Conclusion of Every Hour")
.font(.headline)
Text (hourConclusion)
Divider()
Text (ourFather)
}
.padding()
}
}
.navigationBarTitle ("The Midnight Hour", displayMode: .automatic)
.navigationBarItems (trailing: Button (action: {self.showPreferencesView.toggle()}) {Text (psalmVersions[chosenVersion])}
.sheet(isPresented: $showPreferencesView) {PreferencesView(showPreferencesView: self.$showPreferencesView, chosenVersion: self.$chosenVersion, isPsalmsExpanded: self.$isPsalmsExpanded, showLXX: self.$showLXX)})
}
}
The cause of the NavigationLink not appearing is probably due to the fact that you're including a List within a ScrollView. Because List is a scrolling component as well, the sizing gets messed up and the List ends up with a height of 0. Remove the List { and corresponding } and your links should appear.
There are a number of other potential issues in your code (as I alluded to in my comment), including a NavigationView in the middle of a ScrollView. I'd remove that as well, as you probably have a NavigationView higher up in your view hierarchy already.

How to make every item the same height in a LazyVGrid?

Edit This is a regression in iOS 15 beta. The code works as expected on iOS 14.5:
I have submitted a bug to Apple.
I have a dashboard-style screen in my SwiftUI app, where I am using a LazyVGrid with a single .adaptative column to layout my dashboard widgets, where widgets are laid out in wrapping rows.
It works as I want it to.
However, if a widget happens to be taller than others, I would like other widgets in the same row to grow vertically, so they end up having the same height as the tallest of the row.
This small bit of code illustrates my problem:
struct ContentView: View {
var body: some View {
LazyVGrid(columns: [.init(.adaptive(minimum: 45, maximum: 50), alignment: .top)]) {
VStack {
Spacer()
Text("Hello")
}
.border(.red)
Text("Lorem ipsum")
.border(.blue)
}
.border(.green)
.padding(.horizontal, 100)
}
}
The result is:
I would like the red box (VStack containing Spacer + Hello) to be as tall as the blue box (lorem ipsum).
How could I accomplish that?
Please don't suggest using an HStack, as the above example is only to illustrate my problem with LazyVGrid. I do need to use the grid because I have quite a few children to layout, and the grid works great between phone and iPad form factors (adjusting the number of columns dynamically, exactly as I want it).
It looks like Apple begins (for unknown reason) to apply fixedSize for views in grid to make layout based on known intrinsic content sizes. The Spacer, Shape, Color, etc. do not have intrinsic size so we observe... that what's observed.
A possible approach to resolve this is perform calculations by ourselves (to find dynamically max height and apply it to all cells/views).
Here is a demo (with simple helper wrapper for cell). Tested with Xcode 13.2 / iOS 15.2
struct ContentView: View {
#State private var viewHeight = CGFloat.zero
var body: some View {
LazyVGrid(columns: [.init(.adaptive(minimum: 45, maximum: 50), alignment: .top)]) {
GridCell(height: $viewHeight) {
VStack {
Spacer()
Text("Hello")
}
}.border(.red)
GridCell(height: $viewHeight) {
Text("Lorem ipsum asdfd")
}.border(.blue)
}
.onPreferenceChange(ViewHeightKey.self) {
self.viewHeight = max($0, viewHeight)
}
.border(.green)
.padding(.horizontal, 100)
}
}
struct GridCell<Content: View>: View {
#Binding var height: CGFloat
#ViewBuilder let content: () -> Content
var body: some View {
content()
.frame(minHeight: height)
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value += nextValue()
}
}
I had exactly same problem. My LazyVGrid looked great on iOS 14, but now its items have different heights.
I found a dirty workaround to force the items have same height:
In my case I have only several items in each LazyVGrid (So it won't cause too much performance drop), and it is easy for me to know which item has the largest height. So I made a ZStack and put a transparent highest item behind the actual item.
struct ContentView: View {
var body: some View {
LazyVGrid(columns: [.init(.adaptive(minimum: 45, maximum: 50), alignment: .top)]) {
ZStack {
Text("Lorem ipsum") // This is the highest item.
.opacity(0) // Make it transparent.
Text("Hello")
}
.border(.red)
Text("Lorem ipsum")
.border(.blue)
}
.border(.green)
.padding(.horizontal, 100)
}
}
This workaround works in my case, but I don't recommend using it widely in your app especially when you have a lot of items in the grid.

SwiftUI: presenting modal view with custom transition from frame to frame?

I am trying to replicate what I used to do in UIKit when presenting a ViewController using UIViewControllerTransitioningDelegate and UIViewControllerAnimatedTransitioning.
So, for example, from a view that looks like this:
I want to present a view (I would say a modal view but I am not sure if that is the correct way to go about it in SwiftUI) that grows from the view A into this:
So, I need view B to fade in growing from a frame matching view A into almost full screen. The idea is the user taps on A as if it wanted to expand it into its details (view B).
I looked into SwiftUI transitions, things like this:
extension AnyTransition {
static var moveAndFade: AnyTransition {
let insertion = AnyTransition.move(edge: .trailing)
.combined(with: .opacity)
let removal = AnyTransition.scale
.combined(with: .opacity)
return .asymmetric(insertion: insertion, removal: removal)
}
}
So, I think I need to build a custom transition. But, I am not sure how to go about it yet being new to this.
How would I build a transition to handle the case as described? Being able to have a from frame and a to frame...?
Is this the right way of thinking about it in SwiftUI?
New information:
I have tested matchedGeometryEffect.
Example:
struct TestParentView: View {
#State private var expand = false
#Namespace private var shapeTransition
var body: some View {
VStack {
if expand {
// Rounded Rectangle
Spacer()
RoundedRectangle(cornerRadius: 50.0)
.matchedGeometryEffect(id: "circle", in: shapeTransition)
.frame(minWidth: 0, maxWidth: .infinity, maxHeight: 300)
.padding()
.foregroundColor(Color(.systemGreen))
.animation(.easeIn)
.onTapGesture {
expand.toggle()
}
} else {
// Circle
RoundedRectangle(cornerRadius: 50.0)
.matchedGeometryEffect(id: "circle", in: shapeTransition)
.frame(width: 100, height: 100)
.foregroundColor(Color(.systemOrange))
.animation(.easeIn)
.onTapGesture {
expand.toggle()
}
Spacer()
}
}
}
}
It looks like matchedGeometryEffect could be the tool for the job.
However, even when using matchedGeometryEffect, I still can't solve these two things:
how do I include a fade in / fade out animation?
looking at the behavior of matchedGeometryEffect, when I "close" view B, view B disappears immediately and what we see animating is view A from where B was back to view A's original frame. I actually want view B to scale down to where A is as it fades out.
You would have to use the .matchedGeometryEffect modifier on the two Views that you would like to transition.
Here is an example:
struct MatchedGeometryEffect: View {
#Namespace var nspace
#State private var toggle: Bool = false
var body: some View {
HStack {
if toggle {
VStack {
Rectangle()
.foregroundColor(Color.green)
.matchedGeometryEffect(id: "animation", in: nspace)
.frame(width: 300, height: 300)
Spacer()
}
}
if !toggle {
VStack {
Spacer()
Rectangle()
.foregroundColor(Color.blue)
.matchedGeometryEffect(id: "animation", in: nspace)
.frame(width: 50, height: 50)
}
}
}
.padding()
.overlay(
Button("Switch") { withAnimation(.easeIn(duration: 2)) { toggle.toggle() } }
)
}
}
Image should be a GIF
The main two parts of using this modifier are the id and the namespace.
The id of the two Views you are trying to match have to be the same. They then also have to be in the same namespace. The namespace is declared at the top using the #Namespace property wrapper. In my example I used "animation", but it can really be anything, preferably something that can uniquely identify the Views from other types of animations.
Another important piece of information is that the '''#State''' variable controlling the showing/hiding of Views is animated. This is done through the use of withAnimation { toggle.toggle() }.
I'm also quite new to this, so for some more information you can read this article I found from the Swift-UI Lab:
https://swiftui-lab.com/matchedgeometryeffect-part1/