SwiftUI custom modal view transition lagging

Viewed 439

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()
            }
        }
    }
}
0 Answers
Related