Special Purpose Buttons in SwiftUI

In Swiftui we have three special purpose buttons:

  • EditButton
  • RenameButton
  • PasteButton

EditButton

Clicking on the EditButton, the items of the list are displayed with the delete (if onDelete is defined) and move (if onMove is defined) actions. Take a look at the code:

struct ContentView: View {
    var names = ["nicola", "tom"]
    @State private var pastedText: String = ""
    
    var body: some View {
        NavigationStack {
            List {
                ForEach (names, id:\.self) { name in
                    Text(name)
                    
                }
                .onDelete(perform: {_ in
                    
                })
                .onMove(perform: { indices, newOffset in
                    
                }) 
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
            }
        }
    }
}

Very simple and powerful button.

RenameButton

The RenameButton is displayed as a pencil icon and triggers the rename action.

The code:

struct ContentView: View {
    var names = ["nicola", "tom"]
    @State private var pastedText: String = ""
    
    var body: some View {
        NavigationStack {
            List {
                ForEach (names, id:\.self) { name in
                    Text(name)
                }
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    RenameButton()
                }
            }.renameAction {
                // Your code
            }
        }
    }
}

PasteButton

Clicking on the PasteButton pastes the code that we copied earlier from another place (in this case, it was copied by X).

Take a look at the code:

struct ContentView: View {
    @State private var pastedText: String = "

    var body: some View {
        VStack {
            PasteButton(payloadType: String.self) { strings in
                pastedText = strings[0]
            }
            Divider()
            Text(pastedText)
            Spacer()
        }
    }
}

Thus, tapping the button pastes the item (in this case, a string, which is copied into a string variable).

If you want to copy and paste an image, the code is this:

import SwiftUI
import UniformTypeIdentifiers

struct ContentView: View {
    @State private var image: UIImage?

    var body: some View {
        VStack {
            PasteButton(supportedContentTypes: [UTType.image]) { info in
                for item in info {
                    item.loadObject(ofClass: UIImage.self) { item, error in
                        if let img = item as? UIImage {
                            image = img
                        }
                    }
                }
            }
            Divider()
            Image(uiImage: image ?? .init())
        }
        .padding()
    }
}

Happy buttons.

Note: English is not my native language, so I apologize for any errors. I use AI solely to generate the banner of the post; the content is human-generated.

User Notification in SwiftUI (A Simple Pomodoro Timer)

In this post, we will learn how to create a simple Pomodoro timer using user notifications. The goal is to start a 25-minute timer, and when it expires, we will see a notification on the home screen.

We follow these steps:

  • Define the user permission for local notifications.
  • Take a look at the user notification properties.
  • Implement the app.

Privacy and Permissions

First of all, we have to add the “Privacy – User Notifications Usage Description” to the Info.plist to define the reason for the user notification.

UserNotification

Second, how to declare a UserNotification:

UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
        if success {
            let content = UNMutableNotificationContent()
            content.title = "Pomodoro elapsed"
            content.subtitle = "Take a break"
            content.sound = UNNotificationSound.default
            
            let trigger = UNTimeIntervalNotificationTrigger(timeInterval: Double(25 * 60), repeats: false)
            
            let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
            
            UNUserNotificationCenter.current().add(request)
        } else if let error {
            print(error.localizedDescription)
        }
    }

The first step is to request authorization. This will display a message asking for the user’s approval (only the first time, if the user accepts). If we get authorization, we create the content of the notification with a title, subtitle, and default sound. Next, we create a timer for the notification, create the request for the notification, and add it to the notification center. Note that we create a unique identifier for the notification using UUID().uuidString.

The pomodoro App

The last step is to add a timer to count the elapsed time (for the Pomodoro) and put everything together.

import SwiftUI
import UserNotifications

struct ContentView: View {
    @State var timer: Timer.TimerPublisher = Timer.publish(every: 1, on: .main, in: .common)
    let pomodoroLength = 25 * 60
    @State var counter = 0
    @State var timeString = "25:00"
    @State var hasPermission = false
    
    var body: some View {
        VStack(spacing: 10) {
            Button("Start pomodoro") {
                if hasPermission {
                    counter = 0
                    timeString = "25:00"
                    setTimer()
                    timer.connect()
                    
                    let content = UNMutableNotificationContent()
                    content.title = "Pomodoro elapsed"
                    content.subtitle = "Take a break"
                    content.sound = UNNotificationSound.default
                    
                    let trigger = UNTimeIntervalNotificationTrigger(timeInterval: Double(pomodoroLength), repeats: false)
                    
                    let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: trigger)
                    
                    UNUserNotificationCenter.current().add(request)
                }
            }
            Text("\(timeString)").onReceive(timer, perform: { _ in
                counter += 1
                
                let remaining = pomodoroLength - counter
                let minutes = remaining / 60
                let secs = remaining % 60
                timeString = "\(minutes):\(secs)"
                if counter == pomodoroLength {
                    timer.connect().cancel()
                }
            })
        }.onAppear {
            UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { success, error in
                if success {
                    hasPermission = true
                } else if let error {
                    print(error.localizedDescription)
                }
            }
        }
    }
    func setTimer() {
        self.timer = Timer.publish(every: 1, on: .main, in: .common)
    }
}

We need to import UserNotifications, then define a timer, the Pomodoro length in seconds, the initial string minutes:seconds value, and a counter to count the elapsed time. We also introduce a hasPermission variable that is set when the application starts, so we don’t need to check the authorization every time.

When we tap on “Start Pomodoro”, the notification is created with its timer, and the counter is initialized. Every second, we calculate the remaining time and display it until it equals zero, then the timer is stopped. If everything is set up correctly, we should see a notification.

For testing, I suggest setting the Pomodoro time to a few seconds, but at least enough to lock the screen or switch applications (otherwise you won’t see the notification).

The code https://github.com/niqt/PomodoroApp

Note: English is not my native language, so I apologize for any errors. I use AI solely to generate the banner of the post; the content is human-generated.

Adding Events to the Calendar in SwiftUI Using Default UI (Calendar Series Part III)

In this post, we learn how to use the default view to add an event to the calendar without creating a new one, if we don’t need a customized view. I mean this (sorry, today I’m lazy and don’t want to change my phone’s language to English).

I advise you to take a look at the first post of this series to understand the fundamentals (I won’t repeat them here).

The first step is to create a controller for the view:

class EventUIController: UIViewController, EKEventEditViewDelegate {
    let eventStore = EKEventStore()
    
    func eventEditViewController(_ controller: EKEventEditViewController, didCompleteWith action: EKEventEditViewAction) {
        controller.dismiss(animated: true, completion: nil)
        parent?.dismiss(animated: true, completion: nil)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        Task {
            let response = try await self.eventStore.requestFullAccessToEvents()
            if response {
                let eventController = EKEventEditViewController()
                eventController.eventStore = self.eventStore
                eventController.editViewDelegate = self
                eventController.modalPresentationStyle = .overCurrentContext
                eventController.modalTransitionStyle = .crossDissolve
                self.present(eventController, animated: true, completion: nil)
            }
        }
    }
}

In the viewDidLoad method, we check for authorizations, and if everything is okay, we create the controller, assign the eventStore, set the delegate to itself, and finally, the view is displayed. Note that in this function, we use Task to allow the call of an async action. In another function, eventEditViewController, the dismiss action is managed.

To allow this controller to be used in SwiftUI, we need to have a UIViewControllerRepresentable:

struct EventRepresentable: UIViewControllerRepresentable {
    func makeUIViewController(context: Context) -> EventUIController {
        return EventUIController()
    }
    
    func updateUIViewController(_ uiViewController: EventUIController, context: Context) {
        // Only to be conform to the protocol
    }
}

Now we can call it from SwiftUI.

struct ContentView: View {
    @State var eventsManager = EventStoreManager()
    @State var isPresented = false
    
    var body: some View {
        NavigationStack {
            VStack {
                List(eventsManager.events, id:\.self) { event in
                    Text(event.title)
                }
            }
            .padding()
            .toolbar(content: {
                ToolbarItem(placement: .confirmationAction) {
                    Button("+") {
                        isPresented = true
                    }.font(.largeTitle)
                }
            })
            .sheet(isPresented: $isPresented, content: {
                EventRepresentable()
            })
        }.onAppear {
            Task {
                await loadEvents()
            }
        }.onChange(of: isPresented) {
            if !isPresented {
                Task {
                    await loadEvents()
                }
            }
        }
    }
    
    func loadEvents() async {
        do {
            try await eventsManager.setEventDate(date: Date())
        } catch {
            print("Error")
        }
    }
}

The code is 99% similar to that of the previous post; the only thing that changes is that now the sheet presents the default view instead of our custom view.

With this post, this series is complete (at least for now). Always remember to add permissions in the info.plist. Lastly, if you want to get the list of calendars in the calendar application, simply call the calendars function of the eventStore (this is necessary if you want to add an event to a particular calendar).

The code https://github.com/niqt/CalendarAppExample/tree/defaultEventView

To subscribe to my newsletter [https://nicoladefilippo.com/#mailinglist]

Note: English is not my native language, so I apologize for any errors. I use AI solely to generate the banner of the post; the content is human-generated.

Adding Events to the Calendar in SwiftUI (Calendar Series Part II)

We’ve learned (read-events-from-the-calendar-in-swiftui/ ) how to read calendar events. In this post, we’ll learn how to add an event to the calendar.

We’ll follow these steps:

  1. Add the logic to the events manager.
  2. Create a new view in the app to create an event with a title for the current date.

Let’s start by adding a new function, addEvent, in the EventStoreManager:

func addEvent(date: Date, title: String) {
        let event = EKEvent(eventStore: self.eventStore)
        event.calendar = eventStore.defaultCalendarForNewEvents
        event.title = title
        event.startDate = date
        event.endDate =  Calendar.current.date(byAdding: .hour, value: 1, to: date)
        
        do {
            try self.eventStore.save(event, span: .thisEvent, commit: true)
        } catch {
            print("Saving error \(error)")
        }
    }

(please get the code of the EventStoreManager from the previous post).

In this tutorial, we’ll create an event on a specified date. To simplify, we set the endDate by adding one hour to the startDate and set the title. However, pay special attention to the save function. We pass .thisEvent, which means it’s a single occurrence event and not recurring. The commit parameter is set to true, meaning the event is immediately saved, and we do not need to call the commit function later.

Now, let’s take a look at the ContentView:

struct ContentView: View {
    @State var eventsManager = EventStoreManager()
    @State var isPresented = false
    
    var body: some View {
        NavigationStack {
            VStack {
                List(eventsManager.events, id:\.self) { event in
                    Text(event.title)
                }
            }
            .padding()
            .toolbar(content: {
                ToolbarItem(placement: .confirmationAction) {
                    Button("+") {
                        isPresented = true
                    }.font(.largeTitle)
                }
            })
            .sheet(isPresented: $isPresented, content: {
                EventView()
            })
        }.onAppear {
            Task {
                await loadEvents()
            }
        }.onChange(of: isPresented) {
            if !isPresented {
                Task {
                    await loadEvents()
                }
            }
        }
    }
    
    func loadEvents() async {
        do {
            try await eventsManager.setEventDate(date: Date())
        } catch {
            print("Error")
        }
    }
}

Starting from the end, we see a loadEvent function that sets the date for which we want to load the events. This function is triggered when the view first appears and every time the sheet is closed.

The sheet contains the view to create the new event:

struct EventView: View {
    @State var title = ""
    @State var eventsManager = EventStoreManager()
    @Environment(\.dismiss) private var dismiss
    
    var body: some View {
        VStack {
            TextField("Event title", text: $title)
            Button("Save") {
                eventsManager.addEvent(date: Date(), title: title)
                dismiss()
            }
        }.padding()
    }
}

This view contains only a text field to add the event’s title and a button to call the addEvent function and close the sheet. If you have never used a sheet in SwiftUI, please take a look here https://nicoladefilippo.com/swiftui-sheets-demystified-episode-i/ .

The code can be improved with other best practices, but the purpose of this post is solely to demonstrate how to add an event to the calendar.

The code.

To subscribe to my newsletter [https://nicoladefilippo.com/#mailinglist]

Note: English is not my native language, so I apologize for any errors. I use AI solely to generate the banner of the post; the content is human-generated.

Read events from the calendar in SwiftUI

In this post, we’ll learn how to read events from the calendar in our SwiftUI application. In the next one, we’ll learn how to add an event to the calendar.

For today’s events, I display only the title:

We need to complete two steps:

  1. Add ‘Privacy – Calendars Usage Description’ to the info.plist for full calendar access.
  2. Create an event manager.

The Event Manager

import Foundation
import EventKit

@Observable
class EventStoreManager {
    public var events = [EKEvent]()
    let eventStore = EKEventStore()
    
    func fetchEvent(date: Date) -> [EKEvent] {
        let start = Calendar.current.startOfDay(for: date)
        let end = Calendar.current.date(byAdding: .day, value: 1, to: start) ?? Date()
        let predicate = eventStore.predicateForEvents(withStart: start, end: end, calendars: nil)
        return eventStore.events(matching: predicate)
    }
    
    func authorizationStatus() async throws -> Bool {
        return try await eventStore.requestFullAccessToEvents()
    }
    
    func setEventDate(date: Date) async throws {
        let response = try await authorizationStatus()
        if response {
            self.events = fetchEvent(date: date)
        }
    }
}

First step: Import EventKit. The class is annotated with @Observable to utilize the latest powerful features from iOS 17, allowing the UI to simply reflect changes in this class.

The authorization function is crucial as it requires user permission to access calendar app data. Without this permission, no events can be retrieved.

Events are returned by the fetchEvent function. In this function, the start and end dates are calculated (from 00:00 to 24:00 of the current day in the example). Then, a predicate is created to query the events within that interval.

The ContentView is simple:

struct ContentView: View {
    @State var eventsManager = EventStoreManager()
    var body: some View {
        VStack {
            List(eventsManager.events, id:\.self) { event in
                Text(event.title)
            }
        }.task {
            do {
                try await eventsManager.setEventDate(date: Date())
            } catch {
                print("Error")
            }
        }
        .padding()
    }
}

The eventsManager variable is defined as a @State to capture every change in it (recall that it’s observable). Thus, when the app is displayed, we set the date to search for events (simplified to the current day). The events that are returned are then displayed in a list.

The code is here https://github.com/niqt/CalendarAppExample

To subscribe to my newsletter [https://nicoladefilippo.com/#mailinglist]

Note: English is not my native language, so I apologize for any errors. I use AI solely to generate the banner of the post; the content is human-generated.