I have a custom modal View which is part of the ZStack which overlays the other content when enabled.
When a button is pressed, I want the modal sheet to transition from the bottom edge of the device to the centre of the screen, which I have somewhat accomplished. However, the animation somewhat fails when dismissing the modal view, as seen in the provided video, and I'm having difficulties figuring out why this is.
The animation of the modal view I'm using is
.animation(Animation.spring().speed(1.5))
.transition(.move(edge: .bottom))
For the sake of completion, here is my modal view:
struct AddEventView: View {
@State var eventName: String = ""
@State var endDate = Date().addingTimeInterval(60)
@State var gradientIndex: Int = 0
@EnvironmentObject var model: Model
let existingEvent: Event?
let linearGradients: [LinearGradient] = Gradient.gradients.map {
LinearGradient(
gradient: $0,
startPoint: .topTrailing,
endPoint: .bottomLeading
)
}
/// This closure is invoked when the view is dimissed, with a newly created Event passed as its parameter.
/// If the user cancelled this action, `nil` is passed as the parameter
let onDismiss: (Event?) -> Void
var body: some View {
print("Redrawing AddEventView")
return VStack(spacing: 30.0) {
HStack {
Spacer().frame(width: 44)
Spacer()
Text(existingEvent == nil ? "Create Event" : "Edit Event")
.font(.title3)
.bold()
Spacer()
Button(action: {
onDismiss(nil)
}) {
Image(systemName: "xmark.circle.fill")
.imageScale(.large)
}
.frame(width: 44)
}
.padding(.bottom, 5)
.padding(.top, 8)
HStack {
Text("Name of Event").padding(.trailing, 20)
TextField("My Birthday", text: $eventName)
.frame(height: 35)
}
DatePicker(
"Date of Event".padding(toLength: 19, withPad: " ", startingAt: 0),
selection: $endDate,
in: Date()...
)
.frame(height: 35)
ColorChooser(
linearGradients,
selectedIndex: $gradientIndex
)
.frame(height: 75)
Button(action: {
let adjustedEnd = Calendar.current.date(bySetting: .second, value: 0, of: endDate)
let event = Event(
name: eventName,
start: existingEvent?.start ?? Date(),
end: adjustedEnd!,
gradientIndex: gradientIndex
)
onDismiss(event)
}) {
RoundedRectangle(cornerRadius: 13)
.frame(maxWidth: .infinity)
.frame(height: 42)
.overlay(
Text(existingEvent == nil ? "Add Event" : "Edit Event")
.foregroundColor(.white)
.bold()
)
.padding(.horizontal, 1)
}
.padding(.top, 8)
.disabled(self.eventName.isEmpty)
}
.padding(.all, 16)
.background(Color.white)
.cornerRadius(16)
.shadow(radius: 16)
.onAppear {
if let event = existingEvent {
self.eventName = event.name
self.endDate = event.end
self.gradientIndex = event.gradientIndex
}
}
}
}
and my ContentView:
struct ContentView: View {
@State var progress: Double = 0.0
@State var showModal: Bool = false
@State var showPopover: Bool = false
@State var modifiableEvent: Event?
@State var now: Date = Date()
@State var confettiView = ConfettiUIView()
@EnvironmentObject var model: Model
let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect()
let columns: [GridItem] = Array(repeating: GridItem(.flexible(), spacing: 10), count: 2)
var alertButtons: [Alert.Button] {
return Model.SortableKeyPaths.map { key, _ in
.default(Text(key)) { model.sortedKey = key }
}
}
func onEventEnd() {
self.confettiView.emit(with: [.text("")])
AudioManager.shared.play("Success 1.mp4")
let taptics = UINotificationFeedbackGenerator()
taptics.notificationOccurred(.success)
}
var grid: some View {
LazyVGrid(columns: columns, spacing: 10) {
ForEach(model.events, id: \.self) { event in
SmallCardView(event: event)
.contextMenu {
Button(action: {
modifiableEvent = event
withAnimation {
self.showModal = true
}
}) {
Text("Edit")
Image(systemName: "slider.horizontal.3")
}
Button(action: {
model.removeEvent(event)
}) {
Text("Delete")
Image(systemName: "trash")
}
}
.animation(.linear)
}
if !showModal || modifiableEvent != nil {
AddEventButtonView() {
modifiableEvent = nil
self.showModal = true
}
} else {
Spacer().frame(height: 100)
}
}
.navigationBarTitle(Text("My Events"), displayMode: .large)
.navigationBarItems(
leading: Button(action: { }) {
Image(systemName: "ellipsis")
.imageScale(.large)
},
trailing: Button(action: { self.showPopover = true }) {
Image(systemName: "arrow.up.arrow.down").imageScale(.large)
}
.actionSheet(isPresented: $showPopover) {
ActionSheet(
title: Text("Sort Events"),
buttons: alertButtons + [.cancel()]
)
}
)
}
var body: some View {
return ZStack {
NavigationView {
ScrollView {
grid.padding(.horizontal, 16)
}
.padding(.top)
}
.brightness(self.showModal ? -0.1 : 0)
.blur(radius: self.showModal ? 16 : 0)
.scaleEffect(self.showModal ? 0.95 : 1)
if self.showModal {
AddEventView(existingEvent: modifiableEvent) { event in
if let event = event {
self.model.removeEvent(modifiableEvent)
self.model.addEvent(event)
}
withAnimation {
self.showModal = false
}
}
.padding(.horizontal, 16)
.zIndex(1.0)
.animation(Animation.spring().speed(1.5))
.transition(.move(edge: .bottom))
}
EmptyView().id("\(self.now.hashValue)")
}
.overlay(
UIViewWrapper(view: $confettiView)
.edgesIgnoringSafeArea(.all)
.allowsHitTesting(false)
)
.onReceive(timer) { _ in
if !showModal { self.now = Date() }
if model.events.contains(where: { -1...0 ~= $0.timeRemaining }) {
onEventEnd()
}
}
}
}