Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use swift concurrency to remove hang #190

Merged
merged 2 commits into from
Mar 24, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions reminders-menubar/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ struct RemindersMenuBar: App {
}
}

@MainActor
class AppDelegate: NSObject, NSApplicationDelegate {
static private(set) var shared: AppDelegate!

Expand All @@ -27,6 +28,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {

let popover = NSPopover()
lazy var statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
@MainActor
shp7724 marked this conversation as resolved.
Show resolved Hide resolved
var contentViewController: NSViewController {
let contentView = ContentView()
let remindersData = RemindersData()
Expand Down
129 changes: 71 additions & 58 deletions reminders-menubar/Models/RemindersData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,121 +2,134 @@ import SwiftUI
import Combine
import EventKit

@MainActor
class RemindersData: ObservableObject {
private let userPreferences = UserPreferences.shared

private var cancellationTokens: [AnyCancellable] = []

init() {
addObservers()
update()
Task {
await update()
}
}

private func addObservers() {
NotificationCenter.default.addObserver(self,
selector: #selector(update),
name: .EKEventStoreChanged,
object: nil)

NotificationCenter.default.addObserver(self,
selector: #selector(update),
name: .NSCalendarDayChanged,
object: nil)

cancellationTokens.append(
userPreferences.$menuBarCounterType.dropFirst().sink { [weak self] menuBarCounterType in
let count = self?.getMenuBarCount(menuBarCounterType) ?? -1
self?.updateMenuBarCount(with: count)
NotificationCenter.default.publisher(for: .EKEventStoreChanged)
.sink { [weak self] _ in
self?.update()
}
.store(in: &cancellationTokens)

NotificationCenter.default.publisher(for: .NSCalendarDayChanged)
.sink { [weak self] _ in
self?.update()
}
.store(in: &cancellationTokens)

userPreferences.$menuBarCounterType.dropFirst().sink { [weak self] menuBarCounterType in
guard let self else { return }
Task {
let count = await self.getMenuBarCount(menuBarCounterType)
self.updateMenuBarCount(with: count)
}
)

cancellationTokens.append(
userPreferences.$upcomingRemindersInterval.dropFirst().sink { [weak self] upcomingRemindersInterval in
self?.upcomingReminders = RemindersService.shared.getUpcomingReminders(upcomingRemindersInterval)
}
.store(in: &cancellationTokens)
shp7724 marked this conversation as resolved.
Show resolved Hide resolved

userPreferences.$upcomingRemindersInterval.dropFirst().sink { [weak self] upcomingRemindersInterval in
guard let self else { return }
Task {
self.upcomingReminders = await RemindersService.shared.getUpcomingReminders(upcomingRemindersInterval)
}
)

cancellationTokens.append(
$calendarIdentifiersFilter.dropFirst().sink { [weak self] calendarIdentifiersFilter in
self?.filteredReminderLists = RemindersService.shared.getReminders(of: calendarIdentifiersFilter)
}.store(in: &cancellationTokens)

$calendarIdentifiersFilter.dropFirst().sink { [weak self] calendarIdentifiersFilter in
guard let self else { return }
Task {
self.filteredReminderLists = await RemindersService.shared.getReminders(of: calendarIdentifiersFilter)
}
)

}
.store(in: &cancellationTokens)
}

@Published var calendars: [EKCalendar] = []

@Published var upcomingReminders: [ReminderItem] = []

@Published var filteredReminderLists: [ReminderList] = []

@Published var calendarIdentifiersFilter: [String] = {
guard let identifiers = UserPreferences.shared.preferredCalendarIdentifiersFilter else {
// NOTE: On first use it will load all reminder lists.
let calendars = RemindersService.shared.getCalendars()
let allIdentifiers = calendars.map({ $0.calendarIdentifier })
return allIdentifiers
}

return identifiers
}() {
didSet {
UserPreferences.shared.preferredCalendarIdentifiersFilter = calendarIdentifiersFilter
}
}

@Published var calendarForSaving: EKCalendar? = {
guard RemindersService.shared.authorizationStatus() == .authorized else {
return nil
}

guard let identifier = UserPreferences.shared.preferredCalendarIdentifierForSaving,
let calendar = RemindersService.shared.getCalendar(withIdentifier: identifier) else {
return RemindersService.shared.getDefaultCalendar()
}

return calendar
}() {
didSet {
let identifier = calendarForSaving?.calendarIdentifier
UserPreferences.shared.preferredCalendarIdentifierForSaving = identifier
}
}

@objc func update() {

func update() {
Task { [weak self] in
await self?.update()
}
}
shp7724 marked this conversation as resolved.
Show resolved Hide resolved

func update() async {
let calendars = RemindersService.shared.getCalendars()

let calendarsSet = Set(calendars.map({ $0.calendarIdentifier }))
let calendarIdentifiersFilter = self.calendarIdentifiersFilter.filter({
// NOTE: Checking if calendar in filter still exist
calendarsSet.contains($0)
})

let upcomingRemindersInterval = self.userPreferences.upcomingRemindersInterval
let upcomingReminders = RemindersService.shared.getUpcomingReminders(upcomingRemindersInterval)

let menuBarCount = getMenuBarCount(self.userPreferences.menuBarCounterType)

// TODO: Prefer receive(on:options:) over explicit use of dispatch queues when performing work in subscribers.
// https://developer.apple.com/documentation/combine/fail/receive(on:options:)
DispatchQueue.main.async {
self.calendars = calendars
self.calendarIdentifiersFilter = calendarIdentifiersFilter
self.upcomingReminders = upcomingReminders
self.updateMenuBarCount(with: menuBarCount)
}
let upcomingReminders = await RemindersService.shared.getUpcomingReminders(upcomingRemindersInterval)

let menuBarCount = await getMenuBarCount(self.userPreferences.menuBarCounterType)

self.calendars = calendars
self.calendarIdentifiersFilter = calendarIdentifiersFilter
self.upcomingReminders = upcomingReminders
self.updateMenuBarCount(with: menuBarCount)
}
private func getMenuBarCount(_ menuBarCounterType: RmbMenuBarCounterType) -> Int {

private func getMenuBarCount(_ menuBarCounterType: RmbMenuBarCounterType) async -> Int {
switch menuBarCounterType {
case .today:
return RemindersService.shared.getUpcomingReminders(.today).count
return await RemindersService.shared.getUpcomingReminders(.today).count
case .allReminders:
return RemindersService.shared.getAllRemindersCount()
return await RemindersService.shared.getAllRemindersCount()
case .disabled:
return -1
}
}

private func updateMenuBarCount(with count: Int) {
AppDelegate.shared.updateMenuBarTodayCount(to: count)
}
Expand Down
40 changes: 16 additions & 24 deletions reminders-menubar/Services/RemindersService.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import EventKit

@MainActor
class RemindersService {
static let shared = RemindersService()

Expand Down Expand Up @@ -37,27 +38,18 @@ class RemindersService {
return eventStore.defaultCalendarForNewReminders() ?? eventStore.calendars(for: .reminder).first!
}

private func fetchRemindersSynchronously(matching predicate: NSPredicate) -> [EKReminder] {
var reminders: [EKReminder] = []
// TODO: Remove use of DispatchGroup
let group = DispatchGroup()
group.enter()
eventStore.fetchReminders(matching: predicate) { allReminders in
guard let allReminders else {
print("Reminders was nil during 'fetchReminders'")
group.leave()
return
private func fetchReminders(matching predicate: NSPredicate) async -> [EKReminder] {
await withCheckedContinuation { continuation in
eventStore.fetchReminders(matching: predicate) { allReminders in
guard let allReminders else {
continuation.resume(returning: [])
return
}
continuation.resume(returning: allReminders)
}

reminders = allReminders
group.leave()
}

_ = group.wait(timeout: .distantFuture)

return reminders
}

private func createReminderItems(for calendarReminders: [EKReminder]) -> [ReminderItem] {
var reminderListItems: [ReminderItem] = []

Expand All @@ -73,10 +65,10 @@ class RemindersService {
return reminderListItems
}

func getReminders(of calendarIdentifiers: [String]) -> [ReminderList] {
func getReminders(of calendarIdentifiers: [String]) async -> [ReminderList] {
let calendars = getCalendars().filter({ calendarIdentifiers.contains($0.calendarIdentifier) })
let predicate = eventStore.predicateForReminders(in: calendars)
let remindersByCalendar = Dictionary(grouping: fetchRemindersSynchronously(matching: predicate),
let remindersByCalendar = Dictionary(grouping: await fetchReminders(matching: predicate),
by: { $0.calendar.calendarIdentifier })

var reminderLists: [ReminderList] = []
Expand All @@ -89,20 +81,20 @@ class RemindersService {
return reminderLists
}

func getUpcomingReminders(_ interval: ReminderInterval) -> [ReminderItem] {
func getUpcomingReminders(_ interval: ReminderInterval) async -> [ReminderItem] {
let calendars = getCalendars()
let predicate = eventStore.predicateForIncompleteReminders(withDueDateStarting: nil,
ending: interval.endingDate,
calendars: calendars)
let reminders = fetchRemindersSynchronously(matching: predicate).map({ ReminderItem(for: $0) })
let reminders = await fetchReminders(matching: predicate).map({ ReminderItem(for: $0) })
return reminders.sortedReminders
}

func getAllRemindersCount() -> Int {
func getAllRemindersCount() async -> Int {
let predicate = eventStore.predicateForIncompleteReminders(withDueDateStarting: nil,
ending: nil,
calendars: nil)
let reminders = fetchRemindersSynchronously(matching: predicate)
let reminders = await fetchReminders(matching: predicate)
return reminders.count
}

Expand Down
2 changes: 2 additions & 0 deletions reminders-menubar/Views/ReminderItemView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import SwiftUI
import EventKit

@MainActor
struct ReminderItemView: View {
@EnvironmentObject var remindersData: RemindersData

Expand Down Expand Up @@ -158,6 +159,7 @@ struct ReminderItemView: View {
}
}

@MainActor
struct ChangePriorityOptionMenu: View {
var reminder: EKReminder

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ struct SettingsBarGearMenu: View {
Divider()

Button(action: {
remindersData.update()
Task {
await remindersData.update()
}
}) {
Text(rmbLocalized(.reloadRemindersDataButton))
}
Expand Down