diff --git a/SwiftUI-WorkoutApp.xcodeproj/project.pbxproj b/SwiftUI-WorkoutApp.xcodeproj/project.pbxproj index 79f3f65c..3820c99b 100644 --- a/SwiftUI-WorkoutApp.xcodeproj/project.pbxproj +++ b/SwiftUI-WorkoutApp.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ 671B4AE92D4F623100286996 /* ModernPickedImagesGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671B4AE82D4F623100286996 /* ModernPickedImagesGrid.swift */; }; 671B4AEB2D4F683E00286996 /* ImagePickerViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671B4AEA2D4F683E00286996 /* ImagePickerViews.swift */; }; 671D7DEC28210D2F0068E728 /* EmptyContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671D7DEB28210D2F0068E728 /* EmptyContentView.swift */; }; + 671F17FD2D589E7E005EE522 /* CalendarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671F17FC2D589E7E005EE522 /* CalendarManager.swift */; }; + 671F17FF2D589F45005EE522 /* EKEventEditViewControllerRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671F17FE2D589F45005EE522 /* EKEventEditViewControllerRepresentable.swift */; }; 674000402D55E97900E5CB06 /* BlackListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6740003F2D55E97900E5CB06 /* BlackListScreen.swift */; }; 67419ACF282E70B9004F5339 /* ParksListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67419ACE282E70B9004F5339 /* ParksListScreen.swift */; }; 6747575628113419002F0A24 /* ChangePasswordScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6747575528113419002F0A24 /* ChangePasswordScreen.swift */; }; @@ -106,6 +108,8 @@ 671B4AE82D4F623100286996 /* ModernPickedImagesGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModernPickedImagesGrid.swift; sourceTree = ""; }; 671B4AEA2D4F683E00286996 /* ImagePickerViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerViews.swift; sourceTree = ""; }; 671D7DEB28210D2F0068E728 /* EmptyContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyContentView.swift; sourceTree = ""; }; + 671F17FC2D589E7E005EE522 /* CalendarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarManager.swift; sourceTree = ""; }; + 671F17FE2D589F45005EE522 /* EKEventEditViewControllerRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EKEventEditViewControllerRepresentable.swift; sourceTree = ""; }; 6740003F2D55E97900E5CB06 /* BlackListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlackListScreen.swift; sourceTree = ""; }; 67419ACE282E70B9004F5339 /* ParksListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParksListScreen.swift; sourceTree = ""; }; 6747575528113419002F0A24 /* ChangePasswordScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangePasswordScreen.swift; sourceTree = ""; }; @@ -217,6 +221,8 @@ 6798AA52280AF43900DB76F1 /* EventsListScreen.swift */, 67FBF64E28338A2E008A7968 /* EventDetailsScreen.swift */, 675EC65E2815532800C2E229 /* EventFormScreen.swift */, + 671F17FC2D589E7E005EE522 /* CalendarManager.swift */, + 671F17FE2D589F45005EE522 /* EKEventEditViewControllerRepresentable.swift */, ); path = Events; sourceTree = ""; @@ -615,6 +621,7 @@ 6798AA66280B232F00DB76F1 /* IncognitoProfileView.swift in Sources */, 67BAF3F6283620ED00DB40D9 /* ParkLocationInfoView.swift in Sources */, 67627755283A4C77009C203F /* JournalEntriesScreen.swift in Sources */, + 671F17FD2D589E7E005EE522 /* CalendarManager.swift in Sources */, 674E704E2B24D382008AE9D0 /* LoggerScreen.swift in Sources */, 67B78716281D8006008B104F /* ParksMapScreen+ViewModel.swift in Sources */, 671D7DEC28210D2F0068E728 /* EmptyContentView.swift in Sources */, @@ -640,6 +647,7 @@ 675FB8DB2ADDB87200C9671F /* ParksMapScreen+LocationSettingReminderView.swift in Sources */, 6747575628113419002F0A24 /* ChangePasswordScreen.swift in Sources */, 67515699283FEC3100501346 /* PickedImagesGrid.swift in Sources */, + 671F17FF2D589F45005EE522 /* EKEventEditViewControllerRepresentable.swift in Sources */, 67FBF64F28338A2E008A7968 /* EventDetailsScreen.swift in Sources */, 676D5A612D48BF0700EE5E9E /* String+localized.swift in Sources */, 6765B2562D451771006164AB /* UIImage+toMediaFile.swift in Sources */, @@ -868,9 +876,9 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 14; + CURRENT_PROJECT_VERSION = 15; DEVELOPMENT_ASSET_PATHS = "SwiftUI-WorkoutApp/Preview\\ Content/PreviewContent.swift SwiftUI-WorkoutApp/Preview\\ Content"; - DEVELOPMENT_TEAM = CR68PP2Z3F; + DEVELOPMENT_TEAM = 3PHS45582J; ENABLE_PREVIEWS = YES; GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; @@ -878,6 +886,7 @@ INFOPLIST_FILE = "SwiftUI-WorkoutApp/Resources/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "SW Площадки"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; + INFOPLIST_KEY_NSCalendarsUsageDescription = "Для добавления мероприятий в календарь"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Для отображения спортивных площадок поблизости"; INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Для выбора фото профиля требуется доступ к галерее"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -891,7 +900,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 3.6.0; - PRODUCT_BUNDLE_IDENTIFIER = com.FGU.WorkOut; + PRODUCT_BUNDLE_IDENTIFIER = com.FGU.WorkOut1; PRODUCT_NAME = WorkoutApp; RUN_CLANG_STATIC_ANALYZER = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -919,9 +928,9 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 14; + CURRENT_PROJECT_VERSION = 15; DEVELOPMENT_ASSET_PATHS = "SwiftUI-WorkoutApp/Preview\\ Content/PreviewContent.swift SwiftUI-WorkoutApp/Preview\\ Content"; - DEVELOPMENT_TEAM = CR68PP2Z3F; + DEVELOPMENT_TEAM = 3PHS45582J; ENABLE_PREVIEWS = YES; GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_PARAMETER = YES; @@ -929,6 +938,7 @@ INFOPLIST_FILE = "SwiftUI-WorkoutApp/Resources/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = "SW Площадки"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness"; + INFOPLIST_KEY_NSCalendarsUsageDescription = "Для добавления мероприятий в календарь"; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Для отображения спортивных площадок поблизости"; INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Для выбора фото профиля требуется доступ к галерее"; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; @@ -942,7 +952,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 3.6.0; - PRODUCT_BUNDLE_IDENTIFIER = com.FGU.WorkOut; + PRODUCT_BUNDLE_IDENTIFIER = com.FGU.WorkOut1; PRODUCT_NAME = WorkoutApp; RUN_CLANG_STATIC_ANALYZER = YES; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; diff --git a/SwiftUI-WorkoutApp/Libraries/SWModels/Sources/SWModels/EventResponse.swift b/SwiftUI-WorkoutApp/Libraries/SWModels/Sources/SWModels/EventResponse.swift index d927931d..718fb3af 100644 --- a/SwiftUI-WorkoutApp/Libraries/SWModels/Sources/SWModels/EventResponse.swift +++ b/SwiftUI-WorkoutApp/Libraries/SWModels/Sources/SWModels/EventResponse.swift @@ -6,6 +6,7 @@ public struct EventResponse: Codable, Identifiable, Equatable, Sendable { public let id: Int /// Название мероприятия public var title: String? + /// Описание мероприятия с `html`-тегами public var eventDescription: String? public let fullAddress: String? public var beginDate: String? @@ -147,6 +148,10 @@ public extension EventResponse { previewImageStringURL.queryAllowedURL } + var eventBeginDateForCalendar: Date { + DateFormatterService.dateFromIsoString(beginDate) + } + var eventDateString: String { DateFormatterService.readableDate(from: beginDate) } diff --git a/SwiftUI-WorkoutApp/Resources/InfoPlist.xcstrings b/SwiftUI-WorkoutApp/Resources/InfoPlist.xcstrings index ab8233da..df7fc2f6 100644 --- a/SwiftUI-WorkoutApp/Resources/InfoPlist.xcstrings +++ b/SwiftUI-WorkoutApp/Resources/InfoPlist.xcstrings @@ -32,6 +32,24 @@ }, "shouldTranslate" : false }, + "NSCalendarsUsageDescription" : { + "comment" : "Privacy - Calendars Usage Description", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "For adding events to the calendar" + } + }, + "ru" : { + "stringUnit" : { + "state" : "new", + "value" : "Для добавления мероприятий в календарь" + } + } + } + }, "NSLocationWhenInUseUsageDescription" : { "comment" : "Privacy - Location When In Use Usage Description", "extractionState" : "extracted_with_value", diff --git a/SwiftUI-WorkoutApp/Resources/Localizable.xcstrings b/SwiftUI-WorkoutApp/Resources/Localizable.xcstrings index 77954f8c..c89f62a1 100644 --- a/SwiftUI-WorkoutApp/Resources/Localizable.xcstrings +++ b/SwiftUI-WorkoutApp/Resources/Localizable.xcstrings @@ -1410,6 +1410,16 @@ } } }, + "Добавить в календарь" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add to calendar" + } + } + } + }, "Добавить комментарий" : { "localizations" : { "en" : { @@ -2466,6 +2476,16 @@ } } }, + "Необходимо разрешить полный доступ к календарю в настройках телефона" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "You need to allow full access to the calendar in your phone settings" + } + } + } + }, "Нет запланированных\nмероприятий" : { "localizations" : { "en" : { diff --git a/SwiftUI-WorkoutApp/Screens/Events/CalendarManager.swift b/SwiftUI-WorkoutApp/Screens/Events/CalendarManager.swift new file mode 100644 index 00000000..d87204d9 --- /dev/null +++ b/SwiftUI-WorkoutApp/Screens/Events/CalendarManager.swift @@ -0,0 +1,25 @@ +import EventKit +import Foundation + +final class CalendarManager: ObservableObject { + let eventStore = EKEventStore() + private var hasAccess = false + @Published var showCalendar = false + @Published var showSettingsAlert = false + + @MainActor + func requestAccess() { + switch EKEventStore.authorizationStatus(for: .event) { + case .fullAccess: showCalendar = true + case .restricted, .denied: showSettingsAlert = true + default: + eventStore.requestAccess(to: .event) { [weak self] granted, _ in + DispatchQueue.main.async { + self?.hasAccess = granted + self?.showCalendar = granted + self?.showSettingsAlert = !granted + } + } + } + } +} diff --git a/SwiftUI-WorkoutApp/Screens/Events/EKEventEditViewControllerRepresentable.swift b/SwiftUI-WorkoutApp/Screens/Events/EKEventEditViewControllerRepresentable.swift new file mode 100644 index 00000000..d9e8b8a7 --- /dev/null +++ b/SwiftUI-WorkoutApp/Screens/Events/EKEventEditViewControllerRepresentable.swift @@ -0,0 +1,46 @@ +import EventKit +import EventKitUI +import SwiftUI +import SWModels + +/// Обертка для стандартного календаря - `EKEventEditViewController` +struct EKEventEditViewControllerRepresentable: UIViewControllerRepresentable { + @Environment(\.dismiss) private var dismiss + let eventStore: EKEventStore + let event: EventResponse + + func makeUIViewController(context: Context) -> EKEventEditViewController { + let controller = EKEventEditViewController() + controller.eventStore = eventStore + controller.editViewDelegate = context.coordinator + let eventDate = event.eventBeginDateForCalendar + let ekevent = EKEvent(eventStore: eventStore) + ekevent.title = event.formattedTitle + ekevent.startDate = eventDate + ekevent.endDate = eventDate.addingTimeInterval(3600) // +1 час + ekevent.calendar = eventStore.defaultCalendarForNewEvents + ekevent.location = event.fullAddress + ekevent.notes = event.formattedDescription + ekevent.url = event.shareLinkURL + ekevent.addAlarm(.init(relativeOffset: -3600)) // Напоминание за 1 час + controller.event = ekevent + return controller + } + + func updateUIViewController(_: EKEventEditViewController, context _: Context) {} + + func makeCoordinator() -> Coordinator { .init(parent: self) } + + final class Coordinator: NSObject, @preconcurrency EKEventEditViewDelegate { + private let parent: EKEventEditViewControllerRepresentable + + init(parent: EKEventEditViewControllerRepresentable) { + self.parent = parent + } + + @MainActor + func eventEditViewController(_: EKEventEditViewController, didCompleteWith _: EKEventEditViewAction) { + parent.dismiss() + } + } +} diff --git a/SwiftUI-WorkoutApp/Screens/Events/EventDetailsScreen.swift b/SwiftUI-WorkoutApp/Screens/Events/EventDetailsScreen.swift index 875b56a3..16359fe1 100644 --- a/SwiftUI-WorkoutApp/Screens/Events/EventDetailsScreen.swift +++ b/SwiftUI-WorkoutApp/Screens/Events/EventDetailsScreen.swift @@ -9,6 +9,7 @@ struct EventDetailsScreen: View { @Environment(\.dismiss) private var dismiss @Environment(\.isNetworkConnected) private var isNetworkConnected @EnvironmentObject private var defaults: DefaultsService + @StateObject private var calendarManager = CalendarManager() @State private var navigationDestination: NavigationDestination? @State private var sheetItem: SheetItem? @State private var isLoading = false @@ -17,7 +18,7 @@ struct EventDetailsScreen: View { @State private var deleteCommentTask: Task? @State private var deletePhotoTask: Task? @State private var deleteEventTask: Task? - @State private var refreshButtonTask: Task? + @State private var refreshEventTask: Task? @State var event: EventResponse let onDeletion: (Int) -> Void @@ -25,7 +26,7 @@ struct EventDetailsScreen: View { ScrollView { VStack(spacing: 16) { headerAndMapSection - if showParticipantSection { + if defaults.isAuthorized { participantsSection } if event.hasPhotos { @@ -162,10 +163,32 @@ private extension EventDetailsScreen { address: event.fullAddress ?? shortAddress, appleMapsURL: event.park.appleMapsURL ) + addToCalendarButton } .insideCardBackground() } + var addToCalendarButton: some View { + Button("Добавить в календарь", action: calendarManager.requestAccess) + .buttonStyle(SWButtonStyle(mode: .tinted, size: .large)) + .padding(.top, 12) + .sheet(isPresented: $calendarManager.showCalendar) { + EKEventEditViewControllerRepresentable( + eventStore: calendarManager.eventStore, + event: event + ) + } + .alert( + "Необходимо разрешить полный доступ к календарю в настройках телефона", + isPresented: $calendarManager.showSettingsAlert + ) { + Button("Отмена", role: .cancel) {} + Button("Перейти") { + URLOpener.open(URL(string: UIApplication.openSettingsURLString)) + } + } + } + var descriptionSection: some View { SectionView(headerWithPadding: "Описание", mode: .card(padding: 12)) { Text(.init(event.formattedDescription)) @@ -191,7 +214,7 @@ private extension EventDetailsScreen { ) } } - if event.isCurrent ?? false { + if event.isCurrent == true { FormRowView( title: "Пойду на мероприятие", trailingContent: .toggle( @@ -334,7 +357,7 @@ private extension EventDetailsScreen { func refreshAction() { sheetItem = nil - refreshButtonTask = Task { await askForInfo(refresh: true) } + refreshEventTask = Task { await askForInfo(refresh: true) } } func askForInfo(refresh: Bool = false) async { @@ -406,17 +429,9 @@ private extension EventDetailsScreen { : false } - var showParticipantSection: Bool { - if defaults.isAuthorized { - event.hasParticipants || event.isCurrent ?? false - } else { - false - } - } - func cancelTasks() { [ - refreshButtonTask, + refreshEventTask, deleteCommentTask, goingToEventTask, deletePhotoTask, diff --git a/SwiftUI-WorkoutApp/Screens/Events/EventsListScreen.swift b/SwiftUI-WorkoutApp/Screens/Events/EventsListScreen.swift index 93dc0ede..d5c9eab2 100644 --- a/SwiftUI-WorkoutApp/Screens/Events/EventsListScreen.swift +++ b/SwiftUI-WorkoutApp/Screens/Events/EventsListScreen.swift @@ -18,7 +18,6 @@ struct EventsListScreen: View { @State private var selectedEvent: EventResponse? @State private var showEventCreationSheet = false @State private var showEventCreationRule = false - @State private var eventsTask: Task? private let pastEventStorage = PastEventStorage() var body: some View { @@ -36,9 +35,6 @@ struct EventsListScreen: View { } message: { Text(.init(Constants.Alert.eventCreationRule)) } - .onChange(of: selectedEventType) { _ in - eventsTask = Task { await askForEvents() } - } .onChange(of: defaults.isAuthorized) { isAuth in if !isAuth { selectedEvent = nil } } @@ -55,15 +51,14 @@ struct EventsListScreen: View { .navigationBarTitleDisplayMode(.inline) } .navigationViewStyle(.stack) - .task { await askForEvents() } - .onDisappear { eventsTask?.cancel() } + .task(id: selectedEventType) { await askForEvents() } } } private extension EventsListScreen { var refreshButton: some View { Button { - eventsTask = Task { await askForEvents(refresh: true) } + Task { await askForEvents(refresh: true) } } label: { Icons.Regular.refresh.view } @@ -124,6 +119,7 @@ private extension EventsListScreen { NavigationView { EventDetailsScreen(event: event) { removeEvent(id: $0) } } + .navigationViewStyle(.stack) } } @@ -146,7 +142,7 @@ private extension EventsListScreen { EventFormScreen( mode: .regularCreate, refreshClbk: { - eventsTask = Task { await askForEvents(refresh: true) } + Task { await askForEvents(refresh: true) } } ) .toolbar { @@ -155,6 +151,7 @@ private extension EventsListScreen { } } } + .navigationViewStyle(.stack) } } } diff --git a/SwiftUI-WorkoutApp/Screens/Messages/DialogScreen.swift b/SwiftUI-WorkoutApp/Screens/Messages/DialogScreen.swift index 634144f4..ae8679d5 100644 --- a/SwiftUI-WorkoutApp/Screens/Messages/DialogScreen.swift +++ b/SwiftUI-WorkoutApp/Screens/Messages/DialogScreen.swift @@ -40,11 +40,12 @@ struct DialogScreen: View { } .loadingOverlay(if: isLoading) .background(Color.swBackground) - .task(priority: .low) { await markAsRead() } - .task(priority: .high) { await askForMessages() } - .onDisappear { - [refreshDialogTask, sendMessageTask].forEach { $0?.cancel() } + .task { + async let markAsReadTask: () = markAsRead() + async let askForMessagesTask: () = askForMessages() + _ = await (markAsReadTask, askForMessagesTask) } + .onDisappear { refreshDialogTask?.cancel() } .toolbar { ToolbarItem(placement: .topBarLeading) { refreshButton diff --git a/SwiftUI-WorkoutApp/Screens/Messages/DialogsListScreen.swift b/SwiftUI-WorkoutApp/Screens/Messages/DialogsListScreen.swift index 99b952df..d3afb998 100644 --- a/SwiftUI-WorkoutApp/Screens/Messages/DialogsListScreen.swift +++ b/SwiftUI-WorkoutApp/Screens/Messages/DialogsListScreen.swift @@ -36,7 +36,8 @@ struct DialogsListScreen: View { .onChange(of: defaults.isAuthorized, perform: viewModel.clearDialogsOnLogout) .onChange(of: scenePhase) { phase in if case .active = phase { - refreshTask = Task { await askForDialogs() } + guard refreshTask == nil else { return } + refreshTask = Task { await askForDialogs(refresh: true) } } } .task(id: defaults.isAuthorized) { await askForDialogs() } diff --git a/SwiftUI-WorkoutApp/Screens/Messages/DialogsViewModel.swift b/SwiftUI-WorkoutApp/Screens/Messages/DialogsViewModel.swift index e7f2d109..816c074c 100644 --- a/SwiftUI-WorkoutApp/Screens/Messages/DialogsViewModel.swift +++ b/SwiftUI-WorkoutApp/Screens/Messages/DialogsViewModel.swift @@ -3,13 +3,13 @@ import SWModels import SWNetworkClient import SWUtils +@MainActor final class DialogsViewModel: ObservableObject { @Published private(set) var dialogs = [DialogResponse]() @Published private(set) var isLoading = false var hasDialogs: Bool { !dialogs.isEmpty } var showEmptyView: Bool { !hasDialogs && !isLoading } - @MainActor func askForDialogs( refresh: Bool = false, defaults: DefaultsService @@ -19,23 +19,21 @@ final class DialogsViewModel: ObservableObject { guard dialogs.isEmpty || refresh else { return } if !refresh || dialogs.isEmpty { isLoading = true } dialogs = try await SWClient(with: defaults).getDialogs() - let unreadMessagesCount = dialogs.map(\.unreadMessagesCount).reduce(0, +) - defaults.saveUnreadMessagesCount(unreadMessagesCount) + updateUnreadMessagesCount(with: defaults) isLoading = false } - @MainActor func deleteDialog(at index: Int?, defaults: DefaultsService) async throws { guard let index, !isLoading else { return } isLoading = true let dialogID = dialogs[index].id if try await SWClient(with: defaults).deleteDialog(dialogID) { dialogs.remove(at: index) + updateUnreadMessagesCount(with: defaults) } isLoading = false } - @MainActor func markAsRead(_ dialog: DialogResponse, defaults: DefaultsService) { dialogs = dialogs.map { item in if item.id == dialog.id { @@ -53,9 +51,13 @@ final class DialogsViewModel: ObservableObject { defaults.saveUnreadMessagesCount(newValue) } - @MainActor func clearDialogsOnLogout(isAuthorized: Bool) { guard !isAuthorized else { return } dialogs.removeAll() } + + private func updateUnreadMessagesCount(with defaults: DefaultsService) { + let unreadMessagesCount = dialogs.map(\.unreadMessagesCount).reduce(0, +) + defaults.saveUnreadMessagesCount(unreadMessagesCount) + } } diff --git a/SwiftUI-WorkoutApp/Screens/Parks/Map/ParksMapScreen.swift b/SwiftUI-WorkoutApp/Screens/Parks/Map/ParksMapScreen.swift index 428dc370..2a27a2b8 100644 --- a/SwiftUI-WorkoutApp/Screens/Parks/Map/ParksMapScreen.swift +++ b/SwiftUI-WorkoutApp/Screens/Parks/Map/ParksMapScreen.swift @@ -279,10 +279,12 @@ private extension ParksMapScreen { } } } + .navigationViewStyle(.stack) case let .parkDetails(park): NavigationView { ParkDetailScreen(park: park) { deletePark(id: $0) } } + .navigationViewStyle(.stack) } } } diff --git a/SwiftUI-WorkoutApp/Screens/Parks/ParksListScreen.swift b/SwiftUI-WorkoutApp/Screens/Parks/ParksListScreen.swift index 949140ce..9b9493fc 100644 --- a/SwiftUI-WorkoutApp/Screens/Parks/ParksListScreen.swift +++ b/SwiftUI-WorkoutApp/Screens/Parks/ParksListScreen.swift @@ -13,7 +13,6 @@ struct ParksListScreen: View { @State private var isLoading = false /// Площадка для открытия детального экрана @State private var selectedPark: Park? - @State private var updateParksTask: Task? let mode: Mode var body: some View { @@ -47,6 +46,7 @@ struct ParksListScreen: View { NavigationView { ParkDetailScreen(park: park) { deletePark(id: $0) } } + .navigationViewStyle(.stack) } .onChange(of: parks) { list in if list.isEmpty { @@ -66,7 +66,6 @@ struct ParksListScreen: View { } .navigationTitle(mode.title) .navigationBarTitleDisplayMode(.inline) - .onDisappear { updateParksTask?.cancel() } } } @@ -100,9 +99,7 @@ private extension ParksListScreen { var refreshButtonIfNeeded: some View { if !DeviceOSVersionChecker.iOS16Available { Button { - updateParksTask = Task { - await askForParks(refresh: true) - } + Task { await askForParks(refresh: true) } } label: { Icons.Regular.refresh.view } diff --git a/SwiftUI-WorkoutApp/Screens/Profile/BlackListScreen.swift b/SwiftUI-WorkoutApp/Screens/Profile/BlackListScreen.swift index 241b763a..614e8c81 100644 --- a/SwiftUI-WorkoutApp/Screens/Profile/BlackListScreen.swift +++ b/SwiftUI-WorkoutApp/Screens/Profile/BlackListScreen.swift @@ -112,5 +112,6 @@ private extension BlackListScreen { BlackListScreen() .environmentObject(DefaultsService()) } + .navigationViewStyle(.stack) } #endif diff --git a/SwiftUI-WorkoutApp/Screens/Profile/EditProfile/EditProfileScreen.swift b/SwiftUI-WorkoutApp/Screens/Profile/EditProfile/EditProfileScreen.swift index c69709b8..82311125 100644 --- a/SwiftUI-WorkoutApp/Screens/Profile/EditProfile/EditProfileScreen.swift +++ b/SwiftUI-WorkoutApp/Screens/Profile/EditProfile/EditProfileScreen.swift @@ -280,5 +280,6 @@ private extension EditProfileScreen { EditProfileScreen() .environmentObject(DefaultsService()) } + .navigationViewStyle(.stack) } #endif diff --git a/SwiftUI-WorkoutApp/Screens/Profile/Journals/JournalSettingsScreen.swift b/SwiftUI-WorkoutApp/Screens/Profile/Journals/JournalSettingsScreen.swift index 41a49fd6..49ad616c 100644 --- a/SwiftUI-WorkoutApp/Screens/Profile/Journals/JournalSettingsScreen.swift +++ b/SwiftUI-WorkoutApp/Screens/Profile/Journals/JournalSettingsScreen.swift @@ -11,7 +11,6 @@ struct JournalSettingsScreen: View { @Environment(\.isNetworkConnected) private var isNetworkConnected @State private var journal: JournalResponse @State private var isLoading = false - @State private var saveJournalChangesTask: Task? @FocusState private var isTextFieldFocused: Bool private let options = JournalAccess.allCases private let updateOnSuccess: (JournalResponse) -> Void @@ -39,7 +38,6 @@ struct JournalSettingsScreen: View { } .loadingOverlay(if: isLoading) .interactiveDismissDisabled(isLoading) - .onDisappear { saveJournalChangesTask?.cancel() } } } @@ -86,7 +84,7 @@ private extension JournalSettingsScreen { } func saveChanges() { - saveJournalChangesTask = Task { + Task { isLoading = true do { let isSuccess = try await SWClient(with: defaults).editJournalSettings( diff --git a/SwiftUI-WorkoutApp/Screens/Profile/MainUserProfileScreen.swift b/SwiftUI-WorkoutApp/Screens/Profile/MainUserProfileScreen.swift index 918c81a1..b272726c 100644 --- a/SwiftUI-WorkoutApp/Screens/Profile/MainUserProfileScreen.swift +++ b/SwiftUI-WorkoutApp/Screens/Profile/MainUserProfileScreen.swift @@ -34,9 +34,8 @@ struct MainUserProfileScreen: View { .task { await askForUserInfo() } .onChange(of: scenePhase) { phase in if case .active = phase { - refreshTask = Task { - await askForUserInfo(refresh: true) - } + guard refreshTask == nil else { return } + refreshTask = Task { await askForUserInfo(refresh: true) } } } } @@ -110,6 +109,7 @@ private extension MainUserProfileScreen { NavigationView { SearchUsersScreen() } + .navigationViewStyle(.stack) } } diff --git a/SwiftUI-WorkoutApp/Screens/Settings/SettingsScreen.swift b/SwiftUI-WorkoutApp/Screens/Settings/SettingsScreen.swift index 806f88f0..0e9a9725 100644 --- a/SwiftUI-WorkoutApp/Screens/Settings/SettingsScreen.swift +++ b/SwiftUI-WorkoutApp/Screens/Settings/SettingsScreen.swift @@ -50,6 +50,7 @@ struct SettingsScreen: View { .navigationTitle("Настройки") .navigationBarTitleDisplayMode(.inline) } + .navigationViewStyle(.stack) } } diff --git a/SwiftUI-WorkoutApp/Services/DefaultsService.swift b/SwiftUI-WorkoutApp/Services/DefaultsService.swift index 63e1eaa4..7a5ba232 100644 --- a/SwiftUI-WorkoutApp/Services/DefaultsService.swift +++ b/SwiftUI-WorkoutApp/Services/DefaultsService.swift @@ -85,6 +85,7 @@ final class DefaultsService: ObservableObject, DefaultsProtocol { } func setAppTheme(_ theme: AppColorTheme) { + guard appTheme != theme else { return } appTheme = theme } @@ -130,6 +131,7 @@ final class DefaultsService: ObservableObject, DefaultsProtocol { } func saveUnreadMessagesCount(_ count: Int) { + guard unreadMessagesCount != count else { return } unreadMessagesCount = count } diff --git a/SwiftUI-WorkoutApp/SwiftUI_WorkoutAppApp.swift b/SwiftUI-WorkoutApp/SwiftUI_WorkoutAppApp.swift index 9b144575..995c4569 100644 --- a/SwiftUI-WorkoutApp/SwiftUI_WorkoutAppApp.swift +++ b/SwiftUI-WorkoutApp/SwiftUI_WorkoutAppApp.swift @@ -45,14 +45,15 @@ struct SwiftUI_WorkoutAppApp: App { case .active: updateCountriesIfNeeded() default: - countriesUpdateTask?.cancel() defaults.setUserNeedUpdate(true) } } } private func updateCountriesIfNeeded() { - guard countriesStorage.needUpdate(defaults.lastCountriesUpdateDate) else { return } + guard countriesStorage.needUpdate(defaults.lastCountriesUpdateDate), + countriesUpdateTask == nil + else { return } countriesUpdateTask = Task { if let countries = try? await client.getCountries(), countriesStorage.save(countries) {