GO FURTHER, FASTER: Try the Swift Career Accelerator today! >>

SOLVED: Strange behavior of alert buttons

Forums > SwiftUI

@yoid  

Hello, I need some help, I have a custom alert that based on its value should show a certain message and buttons, but sometimes 'if else' statement in alert action block seems to be totally ignored and just shows an 'OK' button. I would be grateful if someone has a solution to this very strange problem for me. I hope it's clear what I mean, but I'll provide more code if needed.

   

@yoid alerts us all to an issue:

sometimes 'if else' statement in alert action block seems to be totally ignored

First: Source Code!

First, I have not taken a detailed look at this issue you're having. Why? Because you made it very difficult for me to test in my own development environment. Why? Because you included screen shots of code, rather than actual code.

Please help us to help you. Take a moment to review the markup tag for adding code to your forum questions.

See -> Adding Code snips to Forum Messages

Second

I reviewed SwiftUI documentation for initialising an alert via a function. I think you are using this initialiser:

// Initialiser from Apple's documentation
func alert<A, M>(
    _ titleKey: LocalizedStringKey,
    isPresented: Binding<Bool>,
    @ViewBuilder actions: () -> A,
    @ViewBuilder message: () -> M
) -> some View where A : View, M : View

✅ You're providing a titleKey as a localized string.
⁇ You're providing a boolean to present the alert.
✅ You're providing an action closure.
✅ You're providing a message closure.

But you've noticed that the action closure isn't working to your specs?

I don't see all your code. However, your if statement in the action closure shows you comparing viewModel.alert to an enumeration (.deleteAll) and later you set viewModel.alert to nil.

//  Compare to an enum value
if viewModel.alert == .deleteAll // 👈🏼 I think viewModel.alert should be a boolean?

     // ... snip ...

// setting viewModel.alert
viewModel.alert = nil  // 👈🏼 I think viewModel.alert should be false?   

Would like to see how you defined your viewModel to understand its guts.

Third

Put business logic into your view model. Your architecture suggests you're using a view model to handle the state of your application. If so, consider removing your business logic from the view and placing it inside your view model.

Use the view to show the state of your model. Write the view model to expose only the functional intentions of your businss logic. Here, your intention is to delete the pending selections. Hide all the internal steps to do this. Only expose a single intention.

Example:

// Super annoying having to retype your code!

Button("Delete", role: .destructive) {
    // 👇🏼 These are all business rules. Encapsulate them into your viewModel
    viewModel.deleteSelectedWorkingDay( viewModel.pendingSelections) // 👈🏼 view model already knows.
    viewModel.selections.removeAll()
    viewModel.pendingSelections.removeAll()
    viewModel.alert = nil

    // 
    editMode?.wrappedValue = .inactive  // 👈🏼 Not sure about this
}

// Instead consider:
Button("Delete", role: .destructive) {
    // 👇🏼 Detailed steps are protected in the viewModel
    // 👇🏼 Just declare what you want to happen when button is tapped
    viewModel.deletePendingSelections()
}

Keep Coding!

   

@yoid  

Obelix, please accept my apologies and the requested information will be posted below.

View

import SwiftUI

struct WorkingDaysListView: View {

    @ObservedObject var viewModel: WorkingDaysView.ViewModel
    @Environment(\.editMode) var editMode

    var body: some View {
        // MARK: - List
        List(selection: $viewModel.selections) {
            ForEach(viewModel.workingDaysList, id: \.self) { workingDay in
                NavigationLink(destination: DetailView(model: workingDay)) {
                    VStack(alignment: .leading) {
                        HStack(spacing: 10) {
                            DateIconView(model: workingDay)
                            Text(workingDay.wrappedCompanyname)
                                .font(.title3).bold()
                        }
                    }
                    .swipeActions(allowsFullSwipe: false) {
                        Button {
                            viewModel.singeleSelect = workingDay
                            viewModel.alert = .swipeDelete
                        } label: {
                            Label("Delete", systemImage: "trash")
                                .tint(.red)
                        }
                    }
                }
                .listRowSeparator(.hidden)
                .listRowBackground(Color.black.opacity(0))
            }
            //            .onDelete(perform: withAnimation(.smooth) {
            //                viewModel.deleteWorkingDay })
            .onMove(perform: withAnimation(.smooth) {
                viewModel.moveWorkingDay })
        }
        .scrollBounceBehavior(.basedOnSize)
        .scrollContentBackground(.hidden)
        .listRowSpacing(10)
        .confirmationDialog("Want to create a working day with date:", isPresented: $viewModel.confirmationIsShowing, titleVisibility: .visible) {
            Button("Today", action: { viewModel.addWorkingDay() })
            Button("My choose", action: { viewModel.createNewDaySheet = true })
        }
        .sheet(isPresented: $viewModel.createNewDaySheet) {
            CreateNewDayView(viewModel: viewModel)
        }

        // MARK: - Toolbar
        .toolbar {
            ToolbarItem(placement: .topBarLeading) {
                EditButton()
            }

            ToolbarItem(placement: .topBarTrailing) {
                if editMode?.wrappedValue.isEditing == false {
                    Button {
                        viewModel.confirmationIsShowing = true
                    } label: {
                        Label("add", systemImage: "plus.circle.fill")
                    }
                } else {
                    Button(role: .destructive) {
                        viewModel.pendingSelections = viewModel.selections
                        withAnimation {
                            viewModel.alert = .deleteAll
                            editMode?.wrappedValue = .inactive
                        }
                    } label: {
                        Label("Delete", systemImage: "trash")
                    }
                }
            }
        }
        .onAppear {
            // Reset selections when the view appears
            viewModel.selections.removeAll()
            editMode?.wrappedValue = .inactive
        }
        .alert(viewModel.alert?.title ?? "Error Occured" , isPresented: Binding(value: $viewModel.alert)) {
            Button("Delete", role: .destructive) {
                viewModel.handleDeleteAction(editMode)
            }
            Button("Cancel", role: .cancel) {
                viewModel.handleCancelAction(editMode)
            }
        } message: {
            Text(viewModel.alert?.message ?? "")
        }
    }
}

#Preview {
    NavigationStack {
        WorkingDaysListView(viewModel: WorkingDaysView.ViewModel())
    }
}

ViewModel

//
//  WorkingDayViewModel.swift
//  PlusStunde
//
//  Created by Yordan Dimitrov on 24.01.24.
//

import CoreData
import SwiftUI

extension WorkingDaysView {
    @MainActor class ViewModel: ObservableObject {

        // MARK: - Public Properties
        @Published private(set) var workingDaysList: [WorkingDay] = []
        @Published var selections = Set<WorkingDay>()
        @Published var pendingSelections = Set<WorkingDay>()
        @Published var singeleSelect: WorkingDay? = nil
        @Published var alert: CustomAlerts? = nil
        @Published var confirmationIsShowing = false
        @Published var notADayWithTodayDate = false
        @Published var createNewDaySheet = false

        @Published var id = UUID()
        @Published var companyName: String?
        @Published var date = Date()
        @Published var workingHours = 0
        @Published var workOnWeekend = false

        // MARK: - Private Properties
        private var userSettings: UserSettings?
        private let persistenceController = PersistenceController.shared

        // MARK: - Initialization
        init() {
            fetchWorkingDays(filter: nil, sortBy: [NSSortDescriptor(key: "date", ascending: true)])
            fetchUserSettings()
            companyName = userSettings?.companyName
        }

        // MARK: - Public Methods

        /// add a new working day
        func addWorkingDay() {
            fetchUserSettings()
            guard let userSettings else {
                alert = .userDefaultsIsEmpty
                return
            }
            createNewWorkingDay(userSettings: userSettings)
            fetchWorkingDays(filter: nil, sortBy: [NSSortDescriptor(key: "date", ascending: true)])

            persistenceController.save()
        }

        /// Moves a working day within the array.
        func moveWorkingDay(from source: IndexSet, to destination: Int) {
            withAnimation {
                workingDaysList.move(fromOffsets: source, toOffset: destination)
            }
            persistenceController.save()
        }

        /// Deletes a working day at specified offsets.
        func deleteWorkingDay(at offsets: IndexSet) {
            withAnimation {
                guard let index = offsets.first else { return }
                let entity = workingDaysList[index]
                persistenceController.container.viewContext.delete(entity)
                workingDaysList.remove(atOffsets: offsets)
            }
            persistenceController.save()
        }

        /// Deletes a selected working day.
        func swipeDelete(day: WorkingDay) {
            withAnimation {
                guard let index = self.workingDaysList.firstIndex(where: { workingDay in
                    workingDay.wrappedDate == day.wrappedDate
                }) else {return}
                persistenceController.container.viewContext.delete(day)
                workingDaysList.remove(at: index)
            }
            persistenceController.save()
        }

        /// Alert cancel button
        func handleDeleteAction(_ editMode:  Binding<EditMode>?) {
               switch alert {
               case .deleteAll:
                   deleteSelectedWorkingDays(pendingSelections)
                   selections.removeAll()
                   editMode?.wrappedValue = .inactive
                   pendingSelections.removeAll()
               case .swipeDelete:
                   if let selection = singeleSelect {
                       swipeDelete(day: selection)
                       singeleSelect = nil
                   }
               default:
                   break
               }
               alert = nil
           }

        ///  Alert cancel button
         func handleCancelAction(_ editMode:  Binding<EditMode>?) {
            switch alert {
            case .deleteAll:
                withAnimation {
                    editMode?.wrappedValue = .active
                }
                selections = pendingSelections
            default:
                break
            }
            alert = nil
        }

        /// Deletes multiple selected working days.
        func deleteSelectedWorkingDays(_ selection: Set<WorkingDay>) {
            for object in selection {
                if let index = workingDaysList.firstIndex(where: {$0 == object}) {
                    let entity = workingDaysList[index]
                    persistenceController.container.viewContext.delete(entity)
                    workingDaysList.remove(at: index)
                }
            }
            persistenceController.save()
        }

        // MARK: - Private Methods

        /// Creates a new working day based on user settings.
        /// - Parameter userSettings: Predefined user settings for initializing a new working day.
        private func createNewWorkingDay(userSettings: UserSettings) {
                guard !doesDayExist() else {
                    alert = .dayExist
                    return
                }

            let newWorkingDay = WorkingDay(context: persistenceController.container.viewContext)
            newWorkingDay.id = UUID()
            newWorkingDay.companyName = userSettings.companyName
            newWorkingDay.date = notADayWithTodayDate ? date : Date()
            newWorkingDay.workingHours = Int16(notADayWithTodayDate ? workingHours : userSettings.workingHours)
            newWorkingDay.workOnWeekend = notADayWithTodayDate ? workOnWeekend : userSettings.workOnWeekend

            // Add the new WorkingDay object to the list
            withAnimation {
                workingDaysList.append(newWorkingDay)
            }
        }

        /// Fetches user settings from UserDefaults.
        private func fetchUserSettings() {
            guard let userData = UserDefaults.standard.data(forKey: "userSettings") else {
                // THERE ERROR MUST BE HANDLE !!!
                return
            }

            do {
                self.userSettings = try JSONDecoder().decode(UserSettings.self, from: userData)
            } catch {
                print("Failed to decode user settings data:", error.localizedDescription)
                return
            }
        }

        /// Fetches working days with optional filtering and sorting.
        private func fetchWorkingDays(filter: NSPredicate?, sortBy: [NSSortDescriptor]?) {
            workingDaysList = persistenceController.fetchRequest(filter: filter, sortBy: sortBy)
        }

        /// Checks if a working day already exists for the specified date.
       private func doesDayExist() -> Bool {
            let targetDate = notADayWithTodayDate ? self.date : Date()

            let calendar = Calendar.current

            let itemExist = workingDaysList.contains { day in
                return calendar.isDate(day.wrappedDate, inSameDayAs: targetDate)
            }
            return itemExist
        }
    }
}

⁇ You're providing a boolean to present the alert.

I hope this extension is answer of your wondering on Boolean present

import SwiftUI

extension Binding where Value == Bool {

    init<T>(value: Binding<T?>) {
        self.init {
            value.wrappedValue != nil
        } set: { newValue in
            if !newValue  {
                value.wrappedValue = nil
            }
        }
    }
}

The logic specifically for the alert buttons has been moved to the view model as you suggest, let me know if I missed anything.

   

@yoid  

I actually have a view that through an if else statement condition shows Content Unavailable WorkingListView or Content Unavailable and there I've mistakenly attached another alert modifier and that's where the conflict comes from...

I thought about Obelix comment and started to move all the business logic, that's how I found the 'bug' quite by accident.

import CoreData
import SwiftUI

struct WorkingDaysView: View {

    // MARK: Properties
    @StateObject var viewModel = ViewModel()
    private var emptyViewMessage: String = ""

    var body: some View {
        NavigationView {
            // MARK: - Main Stack
            ZStack(alignment: .bottomTrailing) {
                if viewModel.workingDaysList.isEmpty {
                    VStack {
                        ContentUnavailableView {
                            Label("Your list is empty", systemImage: "scribble.variable")
                        } description: {
                            Text(emptyViewMessage)
                        } actions: {
                            Button("Click", action: viewModel.addWorkingDay)
                            .font(.title3)
                            .buttonStyle(BorderedProminentButtonStyle())
                        }
                    }
                } else {
                    // MARK: - ListView
                    WorkingDaysListView(viewModel: viewModel)
                }
            }

           /// A conflict occurs because this alert is duplicated and exists in WorkingDaysListView, after deleting this line of code the alert returns the correct pair of  buttons.
            .alert(viewModel.alert?.title ?? "Error Occured" , isPresented: Binding(value: $viewModel.alert)) {} message: {
                Text(viewModel.alert?.message ?? "")
            }
            .animation(.easeInOut, value: viewModel.workingDaysList.isEmpty)
            .frame(maxHeight: .infinity)
            .navigationTitle("Working Hours")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}

#Preview {
    WorkingDaysView()
}

   

I thought about Obelix comment and started to move all the business logic,
that's how I found the 'bug' quite by accident.

Not an accident! That's a debugging skill!

However, I think the idea of mixing view logic and business logic clouds your mind sometimes.

You may be looking for a logic error, but your eyes are tricked by the view logic. One can hide the other.

Well done finding your bug. Since this is a sharing community, thanks for posting your code and working code. However, It would have been nice for you to point out the flawed logic, and the fixed logic with comments.

You can go back and edit your existing message, and add a few comments on what was incorrect, and what you did to fix the bug!

Well done!

Keep Coding!

   

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's all new Paywall Editor allow you to remotely configure your paywall view without any code changes or app updates.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.