I am trying to make the yellow views disappear from bottom and top with a nice and smooth animation. (slide/move to top/bottom + fadding and keep the middle view taking the whole space)
This is my current state, and it is everything but smooth and nice haha. but it works.
import SwiftUI
struct ContentView: View {
#State var isInterfaceHidden: Bool = false
var body: some View {
VStack(spacing: 0, content: {
if !isInterfaceHidden {
Rectangle()
.id("animation")
.foregroundColor(Color.yellow)
.frame(height: 40)
.transition(AnyTransition.move(edge: .top).combined(with: .opacity).animation(.linear))
}
Rectangle()
.id("animation2")
.foregroundColor(Color.red)
.transition(AnyTransition.opacity.animation(Animation.linear))
/// We make sure it won't cover the top and bottom view.
.zIndex(-1)
.background(Color.red)
.onTapGesture(perform: {
DispatchQueue.main.async {
self.isInterfaceHidden.toggle()
}
})
if !isInterfaceHidden {
Rectangle()
.id("animation3")
.foregroundColor(Color.yellow)
.frame(height: 80)
.transition(AnyTransition.move(edge: .bottom).combined(with: .opacity).animation(.linear))
}
})
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
Current animation:
Edit3: Thanks to #Andrew I was able to progress in a better state.
But now I have a sort of jerky animation.
Any thoughts?
I may have found a solution for you:
import SwiftUI
struct ContentView: View {
#State var isInterfaceHidden: Bool = false
var body: some View {
VStack(spacing: 0, content: {
if !isInterfaceHidden {
Rectangle()
.id("animation")
.foregroundColor(Color.yellow)
.frame(height: 40)
.transition(.topViewTransition)
}
Rectangle()
.id("animation2")
.foregroundColor(Color.red)
/// We make sure it won't cover the top and bottom view.
.zIndex(-1)
.background(Color.red)
.onTapGesture(perform: {
DispatchQueue.main.async {
self.isInterfaceHidden.toggle()
}
})
if !isInterfaceHidden {
Rectangle()
.id("animation3")
.foregroundColor(Color.yellow)
.frame(height: 80)
.transition(.bottomViewTransition)
}
})
.navigationBarTitle("")
.navigationBarHidden(true)
.animation(.easeInOut)
}
}
extension AnyTransition {
static var topViewTransition: AnyTransition {
let transition = AnyTransition.move(edge: .top)
.combined(with: .opacity)
return transition
}
static var bottomViewTransition: AnyTransition {
let transition = AnyTransition.move(edge: .bottom)
.combined(with: .opacity)
return transition
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
Simply set Z index for the both of your yellow views anything higher than the default value of 1.0. This way SwiftUI will make sure they won't be covered by the red view.
The modifier to do that is .zIndex()
A simpler solution
struct Test: View {
#State private var isHiding = false
var body: some View {
ZStack {
Rectangle()
.foregroundColor(.yellow)
.frame(width: 200, height: 100)
Rectangle()
.foregroundColor(.red)
.frame(width: 200, height: isHiding ? 100 : 80)
.onTapGesture {
withAnimation {
self.isHiding.toggle()
}
}
}
}
}
Related
I would like to have my cards expandable and fill the while area of the screen while they are doing the change form height 50 to the whole screen (and don't display the other components)
Here is my code:
import SwiftUI
struct DisciplineView: View {
var body: some View {
ScrollView(showsIndicators: false) {
LazyVStack {
Card(cardTitle: "Notes")
Card(cardTitle: "Planner")
Card(cardTitle: "Homeworks / Exams")
}
.ignoresSafeArea()
}
}
}
struct DisciplineV_Previews: PreviewProvider {
static var previews: some View {
DisciplineView()
}
}
import SwiftUI
struct Card: View {
#State var cardTitle = ""
#State private var isTapped = false
var body: some View {
RoundedRectangle(cornerRadius: 30, style: .continuous)
.stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round))
.foregroundColor(.gray.opacity(0.2))
.frame(width: .infinity, height: isTapped ? .infinity : 50)
.background(
VStack {
cardInfo
if(isTapped) { Spacer() }
}
.padding(isTapped ? 10 : 0)
)
}
var cardInfo: some View {
HStack {
Text(cardTitle)
.font(.title).bold()
.foregroundColor(isTapped ? .white : .black)
.padding(.leading, 10)
Spacer()
Image(systemName: isTapped ? "arrowtriangle.up.square.fill" : "arrowtriangle.down.square.fill")
.padding(.trailing, 10)
.onTapGesture {
withAnimation {
isTapped.toggle()
}
}
}
}
}
struct Card_Previews: PreviewProvider {
static var previews: some View {
Card()
}
}
here is almost the same as I would like to have, but I would like the first one to be on the whole screen and stop the ScrollView while appearing.
Thank you!
Described above:
I would like to have my cards expandable and fill the while area of the screen while they are doing the change form height 50 to the whole screen (and don't display the other components)
I think this is pretty much what you are trying to achieve.
Basically, you have to scroll to the position of the recently presented view and disable the scroll. The scroll have to be disabled enough time to avoid continuing to the next item but at the same time, it have to be enabled soon enough to give the user the feeling that it is scrolling one item at once.
struct ContentView: View {
#State private var canScroll = true
#State private var itemInScreen = -1
var body: some View {
GeometryReader { geo in
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(0...10, id: \.self) { item in
Text("\(item)")
.onAppear {
withAnimation {
proxy.scrollTo(item)
canScroll = false
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
canScroll = true
}
}
}
}
.frame(width: geo.size.width, height: geo.size.height)
.background(Color.blue)
}
}
}
.disabled(!canScroll)
}
.ignoresSafeArea()
}
}
I have two simple ui components a Rectangle and an Image.
I just want a slide animation for Rectangle and scale animation for Image.
However, I got a default value animation for both of these.
Is there a problem in my code? I don't have any error. I'm using beta SF Symbols tho. Could this be the problem?
import SwiftUI
struct ContentView: View {
#State var animate: Bool = false
var body: some View {
VStack(alignment: .center) {
HStack() {
if animate {
ZStack() {
Rectangle()
.frame(height: 50)
.transition(.slide)
Image(systemName: "figure.mixed.cardio")
.foregroundColor(.white)
.transition(.scale)
}
}
}
Button("animate") {
withAnimation {
animate.toggle()
}
}
}
}
}
Transition works when specific view appeared, but in your code ZStack loads views and then appears as a whole.
Here is a fix:
HStack() {
ZStack() {
if animate { // << here !!
Rectangle()
.frame(height: 50)
.transition(.slide)
Image(systemName: "figure.mixed.cardio")
.foregroundColor(.white)
.transition(.scale)
}
}
}
The problem here is neither your SF Symbols or Compiling Error.
Animation is a bit tricky especially if you want different animations for different views inside the same stack.
To achieve your goal, you need to wrap each sub view inside an independent stack, then they will have their own unique animation.
Try this code:
import SwiftUI
struct TestView: View {
#State var animate: Bool = false
var body: some View {
VStack(alignment: .center) {
HStack() {
ZStack() {
//added
if animate {
ZStack {
Rectangle()
.frame(height: 50)
} .transition(.slide)
}
//added
if animate {
ZStack {
Image(systemName: "figure.mixed.cardio")
.foregroundColor(.white)
}.transition(.scale)
}
}
}
Button("animate") {
withAnimation {
animate.toggle()
}
}
}
}
}
I am having problems with making a simple systemIcon flash in SwiftUI.
I got the animation working, but it has a silly behaviour if the layout of
a LazyGridView changes or adapts. Below is a video of its erroneous behaviour.
The flashing bell stays in place but when the layout rearranges the bell
starts transitioning in from the bottom of the parent view thats not there anymore.
Has someone got a suggestion how to get around this?
Here is a working example which is similar to my problem
import SwiftUI
struct FlashingBellLazyVGrid: View {
#State var isAnimating = false
#State var showChart = true
var body: some View {
let columns = [GridItem(.adaptive(minimum: 300), spacing: 50, alignment: .center)]
VStack {
Button(action: {
showChart.toggle()
}) {
VStack {
Circle()
.fill(showChart ? Color.green : Color.red)
.shadow(color: Color.gray, radius: 5, x: 2, y: 2)
Text("Charts")
.foregroundColor(Color.primary)
}.frame(width: 150, height: 50)
}
ScrollView {
LazyVGrid (
columns: columns, spacing: 50
) {
ForEach(0 ..< 25) { item in
ZStack {
Rectangle()
.fill(Color.red)
.cornerRadius(15)
VStack {
HStack {
Text(/*#START_MENU_TOKEN#*/"Hello, World!"/*#END_MENU_TOKEN#*/)
Spacer()
Image(systemName: "bell.fill")
.foregroundColor(Color.yellow)
.opacity(self.isAnimating ? 1 : 0)
.animation(Animation.easeInOut(duration: 0.66).repeatForever(autoreverses: false))
.onAppear{ self.isAnimating = true }
}.padding(50)
if showChart {
Rectangle()
.fill(Color.green)
.frame(height: 200)
}
}
}
}
}
}
}
}
}
struct FlashingBellLazyVGrid_Previews: PreviewProvider {
static var previews: some View {
FlashingBellLazyVGrid()
}
}
how it looks like before you click the showChart button at the top
After you toggle the button it looks like the bells are erroneously moving into place from the bottom of the screen. and toggling it back to its original state doesn't resolve this bug subsequently.
[
Looks like the animation is basing itself off of the original size of the view. In order to trick it into recognizing the new view size, I used .id(UUID()) on the outside of the grid. In a real world application, you'd probably want to be careful to store this ID somewhere and only refresh it when needed -- not on every re-render like I'm doing:
struct FlashingBellLazyVGrid: View {
#State var showChart = true
let columns = [GridItem(.adaptive(minimum: 300), spacing: 50, alignment: .center)]
var body: some View {
VStack {
Button(action: {
showChart.toggle()
}) {
VStack {
Circle()
.fill(showChart ? Color.green : Color.red)
.shadow(color: Color.gray, radius: 5, x: 2, y: 2)
Text("Charts")
.foregroundColor(Color.primary)
}.frame(width: 150, height: 50)
}
ScrollView {
LazyVGrid (
columns: columns, spacing: 50
) {
ForEach(0 ..< 25) { item in
ZStack {
Rectangle()
.fill(Color.red)
.cornerRadius(15)
VStack {
SeparateComponent()
if showChart {
Rectangle()
.fill(Color.green)
.frame(height: 200)
}
}
}
}
}
.id(UUID()) //<-- Here
}
}
}
}
struct SeparateComponent : View {
#State var isAnimating : Bool = false
var body: some View {
HStack {
Text("Hello, World!")
Spacer()
Image(systemName: "bell.fill")
.foregroundColor(Color.yellow)
.opacity(self.isAnimating ? 1 : 0)
.animation(Animation.easeInOut(duration: 0.66).repeatForever(autoreverses: false))
.onAppear{
self.isAnimating = true
}
}
.padding(50)
}
}
I also separated out the blinking component into its own view, since there were already problematic things happening with the existing logic with onAppear, which wouldn't affect newly-scrolled-to items correctly. This may need refactoring for your particular case as well, but this should get you started.
I have a list view embedded in a Navigation View, however, this Navigation View is only about the screen height. This list links to a detailed view, and when a row is tapped, the Detail View only takes up half of the screen. I would like it to open a completely new window.
Screenshots:
The code is used is the following:
import SwiftUI
import CoreData
extension UIScreen{
static let screenWidth = UIScreen.main.bounds.size.width
static let screenHeight = UIScreen.main.bounds.size.height
static let screenSize = UIScreen.main.bounds.size
}
let topCardHeight: CGFloat = 350
struct HomeView: View {
#Environment(\.managedObjectContext) var moc
#FetchRequest(entity: SavedPoem.entity(), sortDescriptors: []) var savedpoems : FetchedResults<SavedPoem>
var body: some View {
VStack {
VStack (alignment: .center){
Text("Today's Poem, November 18th...")
.font(.subheadline)
.foregroundColor(.white)
.padding(.bottom)
.padding(.top, 75)
Text("No Man Is An Island")
.font(.largeTitle)
.fontWeight(.heavy)
.foregroundColor(.white)
.padding(.bottom,1)
Text("by John Donne")
.font(.largeTitle)
.fontWeight(.heavy)
.foregroundColor(.white)
.padding(.bottom, 35)
Button(action: {}) {
Text("Read Now")
.fontWeight(/*#START_MENU_TOKEN#*/.bold/*#END_MENU_TOKEN#*/)
.font(.subheadline)
.foregroundColor(.white)
.padding(15)
.border(Color.white, width: 3)
}
}
.frame(width: UIScreen.screenWidth, height: topCardHeight, alignment: .top)
.background(Color.black)
.edgesIgnoringSafeArea(.top)
.edgesIgnoringSafeArea(.bottom)
.padding(.bottom, 0)
NavigationView{
List{
ForEach(savedpoems, id:\.title) {SavedPoem in
NavigationLink (destination: ContentView()){
ZStack {
Rectangle().fill(Color.white)
.frame(width: UIScreen.main.bounds.width - 32, height: 70)
.cornerRadius(10).shadow(color: .gray, radius: 4)
HStack {
VStack (alignment: .leading){
Text("\(SavedPoem.title ?? "")").font(.headline)
.lineLimit(1)
Text("\(SavedPoem.author ?? "")").font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
}.padding()
}
// }.onDelete(perform: remove)
}
}
}
.navigationTitle("My Saved Poems")
.navigationBarHidden(true)
.edgesIgnoringSafeArea(.top)
.padding(.top, 0)
}
}
// func remove(at offsets : IndexSet) {
// for index in offsets {
// let delete = SavedPoem[index]
// self.moc.delete(delete)
// }
// try? self.moc.save()
// }
}
Any ideas? Thanks in advance.
If you need the same UI:
(The navigation view at the bottom of your top view) , here is a solution for it .
var body: some View {
#EnvironmentObject var sharedViewModel : SharedViewModel
VStack {
VStack (alignment: .center){
if sharedViewModel.currentPageIsHome {
// your top view body here ..
}
}
NavigationView{\*....*\}
}
}.onAppear {
sharedViewModel.currentPageIsHome = true
}.onDisappear {
sharedViewModel.currentPageIsHome = false
}
And you need to create an Observable object
class SharedViewModel: ObservableObject {
#Published var currentPageIsHome = false
}
And don't forget to initialize it in your SceneDelegate
ContentView().environmentObject(SharedViewModel())
Or
Clear version :
change your view hierarchy to :
NavigationView {
List{
Section(header: YourTopView()) {
// ... your list content
}
}
}
My goal is to have custom modals present over an root view that is essentially a tabbed view. So, I wrapped the TabView in a ZStack and am using an ObservableOBject. But I don't feel I'm doing it the right way.
In my other file, I have the Custom modal "subviews" which has an enum, too, which I think is the right approach to take. But I cannot figure out how to dismiss a modal after it is visible.
It must be #EnvironmentObject, but I don't know what if anything to put in the scene delegate, etc. ("Hacking with Swift" is failing me here, although it's a great resource.)
My idea is that views from the tabbed view will have various buttons which present different modal views, populated later with data specific to say a user and set of fields for data entry.
Right now, I just want to understand how to present and dismiss them.
Here is my root view
import SwiftUI
struct ContentView: View {
#ObservedObject var modal = CustomModal()
var body: some View {
ZStack {
TabView {
ZStack {
Color.pink.opacity(0.2)
Button(action: {
withAnimation{
self.modal.visibleModal = VisibleModal.circle
}
}) {
Text("Circle").font(.headline)
}
.frame(width: 270, height: 64)
.background(Color.pink.opacity(0.5)).foregroundColor(.white)
.cornerRadius(12)
}
.tabItem{
VStack{
Image(systemName: "1.square.fill")
Text("One")
}
}.tag(1)
ZStack {
Color.blue.opacity(0.2)
Button(action: {
self.modal.visibleModal = VisibleModal.squircle
}) {
Text("Square").font(.headline)
}
.frame(width: 270, height: 64)
.background(Color.blue.opacity(0.5)).foregroundColor(.white)
.cornerRadius(12)
}
.tabItem{
VStack{
Image(systemName: "2.square.fill")
Text("Two")
}
}.tag(2)
}.accentColor(.purple)
VStack {
containedView()
}
}
}
func containedView() -> AnyView {
switch modal.visibleModal {
case .circle: return AnyView(CircleView())
case .squircle: return AnyView(SquircleView())
case .none: return AnyView(Text(""))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
And here is my second file with the enum and "subview" modals
import SwiftUI
class CustomModal: ObservableObject {
#Published var visibleModal: VisibleModal = VisibleModal.none
}
enum VisibleModal {
case circle, squircle, none
}
struct CircleView: View {
var body: some View {
ZStack {
Color.pink.blur(radius: 0.4)
Circle().fill()
.frame(width: 300)
.foregroundColor(Color.white.opacity(0.75))
dismissButton()
}.edgesIgnoringSafeArea(.all)
}
}
struct SquircleView: View {
var body: some View {
ZStack{
Color.green.blur(radius: 0.4)
RoundedRectangle(cornerRadius: 48, style: .continuous)
.frame(width: 300, height: 300).foregroundColor(Color.white.opacity(0.75))
dismissButton()
}.edgesIgnoringSafeArea(.all)
}
}
struct dismissButton: View {
#ObservedObject var modal = CustomModal()
var body: some View {
VStack{
Spacer()
Button(action: {
self.modal.visibleModal = VisibleModal.none
}) {
Text("Dismiss").font(.headline)
}
.frame(width: 270, height: 64)
.background(Color.white.opacity(0.35)).foregroundColor(.white)
.cornerRadius(12)
.padding(.bottom, 44)
}
}
}
Are you just trying to pass your observable object to the new view?
func containedView() -> some View {
switch modal.visibleModal {
case .circle: return CircleView()
.environmentObject(self.modal)
case .squircle: return SquircleView()
.environmentObject(self.modal)
case .none: return Text("")
}
}
Unless I am misunderstanding the question.
Okay, after a lot of fiddling, it works.
Now my code is as follows.
Root view
struct ContentView: View {
#EnvironmentObject var isModalVisible: CustomModal
#ObservedObject var modal = CustomModal()
var body: some View {
ZStack {
TabView {
ZStack {
Color.pink.opacity(0.2)
Button(action: {
withAnimation{
self.isModalVisible.isModalVisible.toggle()
self.modal.currentModal = VisibleModal.circle
}
}) {
Text("Circle").font(.headline)
}
.frame(width: 270, height: 64)
.background(Color.pink.opacity(0.5)).foregroundColor(.white)
.cornerRadius(12)
}
.tabItem{
VStack{
Image(systemName: "1.square.fill")
Text("One")
}
}.tag(1)
ZStack {
Color.blue.opacity(0.2)
Button(action: {
self.isModalVisible.isModalVisible.toggle()
self.modal.currentModal = VisibleModal.squircle
}) {
Text("Square").font(.headline)
}
.frame(width: 270, height: 64)
.background(Color.blue.opacity(0.5)).foregroundColor(.white)
.cornerRadius(12)
}
.tabItem{
VStack{
Image(systemName: "2.square.fill")
Text("Two")
}
}.tag(2)
}.accentColor(.purple)
if self.isModalVisible.isModalVisible {
VStack {
containedView()
}
}
}
}
func containedView() -> AnyView {
switch modal.currentModal {
case .circle: return AnyView(CircleView())
case .squircle: return AnyView(SquircleView())
case .none: return AnyView(Text(""))
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environmentObject(CustomModal())
}
}
and the second file with the supporting views and classes and enums:
import SwiftUI
class CustomModal: ObservableObject {
#Published var isModalVisible = false
#Published var currentModal: VisibleModal = .none
}
enum VisibleModal {
case circle, squircle, none
}
struct CircleView: View {
#EnvironmentObject var env: CustomModal
var body: some View {
ZStack {
Color.pink.blur(radius: 0.4)
Circle().fill()
.frame(width: 300)
.foregroundColor(Color.white.opacity(0.75))
dismissButton()
}.edgesIgnoringSafeArea(.all)
}
}
struct SquircleView: View {
var body: some View {
ZStack{
Color.green.blur(radius: 0.4)
RoundedRectangle(cornerRadius: 48, style: .continuous)
.frame(width: 300, height: 300).foregroundColor(Color.white.opacity(0.75))
dismissButton()
}.edgesIgnoringSafeArea(.all)
}
}
struct dismissButton: View {
#EnvironmentObject var env: CustomModal
var body: some View {
VStack{
Spacer()
Button(action: {
self.env.isModalVisible.toggle()
print("TAPPED")
}) {
Text("Dismiss").font(.headline)
}
.frame(width: 270, height: 64)
.background(Color.white.opacity(0.35)).foregroundColor(.white)
.cornerRadius(12)
.padding(.bottom, 44)
}
}
}
It still can be refactored. I'm sure. I'd also be happy to hear any comments on how to improve it. But it seems to work.
NOTE: This code ContentView().environmentObject(CustomModal()) is put in the previewP{rovider code and in SceneDelegate.