Skip to content

Commit 83f82e8

Browse files
authored
Доработки и рефактор (#282)
* Рефактор и доработки - Некоторые запросы нет смысла отменять - убрал эту отмену - Добавил всем `NavigationView` стиль `stack` - При удалении диалога обновляем количество непрочитанных сообщений - При переходе в `active`-фазу отправляем запросы для обновления информации при условии, что соответствующие таски еще не созданы - Поправил загрузку чатов при открытии приложения * Добавление мероприятия в календарь На детальном экране с мероприятием можно добавить это мероприятие в календарь с напоминанием за 1 час до начала
1 parent 9fe4d57 commit 83f82e8

20 files changed

+194
-51
lines changed

SwiftUI-WorkoutApp.xcodeproj/project.pbxproj

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
671B4AE92D4F623100286996 /* ModernPickedImagesGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671B4AE82D4F623100286996 /* ModernPickedImagesGrid.swift */; };
1717
671B4AEB2D4F683E00286996 /* ImagePickerViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671B4AEA2D4F683E00286996 /* ImagePickerViews.swift */; };
1818
671D7DEC28210D2F0068E728 /* EmptyContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671D7DEB28210D2F0068E728 /* EmptyContentView.swift */; };
19+
671F17FD2D589E7E005EE522 /* CalendarManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671F17FC2D589E7E005EE522 /* CalendarManager.swift */; };
20+
671F17FF2D589F45005EE522 /* EKEventEditViewControllerRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 671F17FE2D589F45005EE522 /* EKEventEditViewControllerRepresentable.swift */; };
1921
674000402D55E97900E5CB06 /* BlackListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6740003F2D55E97900E5CB06 /* BlackListScreen.swift */; };
2022
67419ACF282E70B9004F5339 /* ParksListScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67419ACE282E70B9004F5339 /* ParksListScreen.swift */; };
2123
6747575628113419002F0A24 /* ChangePasswordScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6747575528113419002F0A24 /* ChangePasswordScreen.swift */; };
@@ -106,6 +108,8 @@
106108
671B4AE82D4F623100286996 /* ModernPickedImagesGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModernPickedImagesGrid.swift; sourceTree = "<group>"; };
107109
671B4AEA2D4F683E00286996 /* ImagePickerViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerViews.swift; sourceTree = "<group>"; };
108110
671D7DEB28210D2F0068E728 /* EmptyContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyContentView.swift; sourceTree = "<group>"; };
111+
671F17FC2D589E7E005EE522 /* CalendarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarManager.swift; sourceTree = "<group>"; };
112+
671F17FE2D589F45005EE522 /* EKEventEditViewControllerRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EKEventEditViewControllerRepresentable.swift; sourceTree = "<group>"; };
109113
6740003F2D55E97900E5CB06 /* BlackListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlackListScreen.swift; sourceTree = "<group>"; };
110114
67419ACE282E70B9004F5339 /* ParksListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParksListScreen.swift; sourceTree = "<group>"; };
111115
6747575528113419002F0A24 /* ChangePasswordScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangePasswordScreen.swift; sourceTree = "<group>"; };
@@ -217,6 +221,8 @@
217221
6798AA52280AF43900DB76F1 /* EventsListScreen.swift */,
218222
67FBF64E28338A2E008A7968 /* EventDetailsScreen.swift */,
219223
675EC65E2815532800C2E229 /* EventFormScreen.swift */,
224+
671F17FC2D589E7E005EE522 /* CalendarManager.swift */,
225+
671F17FE2D589F45005EE522 /* EKEventEditViewControllerRepresentable.swift */,
220226
);
221227
path = Events;
222228
sourceTree = "<group>";
@@ -615,6 +621,7 @@
615621
6798AA66280B232F00DB76F1 /* IncognitoProfileView.swift in Sources */,
616622
67BAF3F6283620ED00DB40D9 /* ParkLocationInfoView.swift in Sources */,
617623
67627755283A4C77009C203F /* JournalEntriesScreen.swift in Sources */,
624+
671F17FD2D589E7E005EE522 /* CalendarManager.swift in Sources */,
618625
674E704E2B24D382008AE9D0 /* LoggerScreen.swift in Sources */,
619626
67B78716281D8006008B104F /* ParksMapScreen+ViewModel.swift in Sources */,
620627
671D7DEC28210D2F0068E728 /* EmptyContentView.swift in Sources */,
@@ -640,6 +647,7 @@
640647
675FB8DB2ADDB87200C9671F /* ParksMapScreen+LocationSettingReminderView.swift in Sources */,
641648
6747575628113419002F0A24 /* ChangePasswordScreen.swift in Sources */,
642649
67515699283FEC3100501346 /* PickedImagesGrid.swift in Sources */,
650+
671F17FF2D589F45005EE522 /* EKEventEditViewControllerRepresentable.swift in Sources */,
643651
67FBF64F28338A2E008A7968 /* EventDetailsScreen.swift in Sources */,
644652
676D5A612D48BF0700EE5E9E /* String+localized.swift in Sources */,
645653
6765B2562D451771006164AB /* UIImage+toMediaFile.swift in Sources */,
@@ -868,16 +876,17 @@
868876
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
869877
CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES;
870878
CODE_SIGN_STYLE = Automatic;
871-
CURRENT_PROJECT_VERSION = 14;
879+
CURRENT_PROJECT_VERSION = 15;
872880
DEVELOPMENT_ASSET_PATHS = "SwiftUI-WorkoutApp/Preview\\ Content/PreviewContent.swift SwiftUI-WorkoutApp/Preview\\ Content";
873-
DEVELOPMENT_TEAM = CR68PP2Z3F;
881+
DEVELOPMENT_TEAM = 3PHS45582J;
874882
ENABLE_PREVIEWS = YES;
875883
GCC_WARN_UNUSED_LABEL = YES;
876884
GCC_WARN_UNUSED_PARAMETER = YES;
877885
GENERATE_INFOPLIST_FILE = YES;
878886
INFOPLIST_FILE = "SwiftUI-WorkoutApp/Resources/Info.plist";
879887
INFOPLIST_KEY_CFBundleDisplayName = "SW Площадки";
880888
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness";
889+
INFOPLIST_KEY_NSCalendarsUsageDescription = "Для добавления мероприятий в календарь";
881890
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Для отображения спортивных площадок поблизости";
882891
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Для выбора фото профиля требуется доступ к галерее";
883892
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@@ -891,7 +900,7 @@
891900
"@executable_path/Frameworks",
892901
);
893902
MARKETING_VERSION = 3.6.0;
894-
PRODUCT_BUNDLE_IDENTIFIER = com.FGU.WorkOut;
903+
PRODUCT_BUNDLE_IDENTIFIER = com.FGU.WorkOut1;
895904
PRODUCT_NAME = WorkoutApp;
896905
RUN_CLANG_STATIC_ANALYZER = YES;
897906
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
@@ -919,16 +928,17 @@
919928
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
920929
CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES;
921930
CODE_SIGN_STYLE = Automatic;
922-
CURRENT_PROJECT_VERSION = 14;
931+
CURRENT_PROJECT_VERSION = 15;
923932
DEVELOPMENT_ASSET_PATHS = "SwiftUI-WorkoutApp/Preview\\ Content/PreviewContent.swift SwiftUI-WorkoutApp/Preview\\ Content";
924-
DEVELOPMENT_TEAM = CR68PP2Z3F;
933+
DEVELOPMENT_TEAM = 3PHS45582J;
925934
ENABLE_PREVIEWS = YES;
926935
GCC_WARN_UNUSED_LABEL = YES;
927936
GCC_WARN_UNUSED_PARAMETER = YES;
928937
GENERATE_INFOPLIST_FILE = YES;
929938
INFOPLIST_FILE = "SwiftUI-WorkoutApp/Resources/Info.plist";
930939
INFOPLIST_KEY_CFBundleDisplayName = "SW Площадки";
931940
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.healthcare-fitness";
941+
INFOPLIST_KEY_NSCalendarsUsageDescription = "Для добавления мероприятий в календарь";
932942
INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "Для отображения спортивных площадок поблизости";
933943
INFOPLIST_KEY_NSPhotoLibraryUsageDescription = "Для выбора фото профиля требуется доступ к галерее";
934944
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@@ -942,7 +952,7 @@
942952
"@executable_path/Frameworks",
943953
);
944954
MARKETING_VERSION = 3.6.0;
945-
PRODUCT_BUNDLE_IDENTIFIER = com.FGU.WorkOut;
955+
PRODUCT_BUNDLE_IDENTIFIER = com.FGU.WorkOut1;
946956
PRODUCT_NAME = WorkoutApp;
947957
RUN_CLANG_STATIC_ANALYZER = YES;
948958
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";

SwiftUI-WorkoutApp/Libraries/SWModels/Sources/SWModels/EventResponse.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ public struct EventResponse: Codable, Identifiable, Equatable, Sendable {
66
public let id: Int
77
/// Название мероприятия
88
public var title: String?
9+
/// Описание мероприятия с `html`-тегами
910
public var eventDescription: String?
1011
public let fullAddress: String?
1112
public var beginDate: String?
@@ -147,6 +148,10 @@ public extension EventResponse {
147148
previewImageStringURL.queryAllowedURL
148149
}
149150

151+
var eventBeginDateForCalendar: Date {
152+
DateFormatterService.dateFromIsoString(beginDate)
153+
}
154+
150155
var eventDateString: String {
151156
DateFormatterService.readableDate(from: beginDate)
152157
}

SwiftUI-WorkoutApp/Resources/InfoPlist.xcstrings

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,24 @@
3232
},
3333
"shouldTranslate" : false
3434
},
35+
"NSCalendarsUsageDescription" : {
36+
"comment" : "Privacy - Calendars Usage Description",
37+
"extractionState" : "extracted_with_value",
38+
"localizations" : {
39+
"en" : {
40+
"stringUnit" : {
41+
"state" : "translated",
42+
"value" : "For adding events to the calendar"
43+
}
44+
},
45+
"ru" : {
46+
"stringUnit" : {
47+
"state" : "new",
48+
"value" : "Для добавления мероприятий в календарь"
49+
}
50+
}
51+
}
52+
},
3553
"NSLocationWhenInUseUsageDescription" : {
3654
"comment" : "Privacy - Location When In Use Usage Description",
3755
"extractionState" : "extracted_with_value",

SwiftUI-WorkoutApp/Resources/Localizable.xcstrings

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1410,6 +1410,16 @@
14101410
}
14111411
}
14121412
},
1413+
"Добавить в календарь" : {
1414+
"localizations" : {
1415+
"en" : {
1416+
"stringUnit" : {
1417+
"state" : "translated",
1418+
"value" : "Add to calendar"
1419+
}
1420+
}
1421+
}
1422+
},
14131423
"Добавить комментарий" : {
14141424
"localizations" : {
14151425
"en" : {
@@ -2466,6 +2476,16 @@
24662476
}
24672477
}
24682478
},
2479+
"Необходимо разрешить полный доступ к календарю в настройках телефона" : {
2480+
"localizations" : {
2481+
"en" : {
2482+
"stringUnit" : {
2483+
"state" : "translated",
2484+
"value" : "You need to allow full access to the calendar in your phone settings"
2485+
}
2486+
}
2487+
}
2488+
},
24692489
"Нет запланированных\nмероприятий" : {
24702490
"localizations" : {
24712491
"en" : {
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import EventKit
2+
import Foundation
3+
4+
final class CalendarManager: ObservableObject {
5+
let eventStore = EKEventStore()
6+
private var hasAccess = false
7+
@Published var showCalendar = false
8+
@Published var showSettingsAlert = false
9+
10+
@MainActor
11+
func requestAccess() {
12+
switch EKEventStore.authorizationStatus(for: .event) {
13+
case .fullAccess: showCalendar = true
14+
case .restricted, .denied: showSettingsAlert = true
15+
default:
16+
eventStore.requestAccess(to: .event) { [weak self] granted, _ in
17+
DispatchQueue.main.async {
18+
self?.hasAccess = granted
19+
self?.showCalendar = granted
20+
self?.showSettingsAlert = !granted
21+
}
22+
}
23+
}
24+
}
25+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import EventKit
2+
import EventKitUI
3+
import SwiftUI
4+
import SWModels
5+
6+
/// Обертка для стандартного календаря - `EKEventEditViewController`
7+
struct EKEventEditViewControllerRepresentable: UIViewControllerRepresentable {
8+
@Environment(\.dismiss) private var dismiss
9+
let eventStore: EKEventStore
10+
let event: EventResponse
11+
12+
func makeUIViewController(context: Context) -> EKEventEditViewController {
13+
let controller = EKEventEditViewController()
14+
controller.eventStore = eventStore
15+
controller.editViewDelegate = context.coordinator
16+
let eventDate = event.eventBeginDateForCalendar
17+
let ekevent = EKEvent(eventStore: eventStore)
18+
ekevent.title = event.formattedTitle
19+
ekevent.startDate = eventDate
20+
ekevent.endDate = eventDate.addingTimeInterval(3600) // +1 час
21+
ekevent.calendar = eventStore.defaultCalendarForNewEvents
22+
ekevent.location = event.fullAddress
23+
ekevent.notes = event.formattedDescription
24+
ekevent.url = event.shareLinkURL
25+
ekevent.addAlarm(.init(relativeOffset: -3600)) // Напоминание за 1 час
26+
controller.event = ekevent
27+
return controller
28+
}
29+
30+
func updateUIViewController(_: EKEventEditViewController, context _: Context) {}
31+
32+
func makeCoordinator() -> Coordinator { .init(parent: self) }
33+
34+
final class Coordinator: NSObject, @preconcurrency EKEventEditViewDelegate {
35+
private let parent: EKEventEditViewControllerRepresentable
36+
37+
init(parent: EKEventEditViewControllerRepresentable) {
38+
self.parent = parent
39+
}
40+
41+
@MainActor
42+
func eventEditViewController(_: EKEventEditViewController, didCompleteWith _: EKEventEditViewAction) {
43+
parent.dismiss()
44+
}
45+
}
46+
}

SwiftUI-WorkoutApp/Screens/Events/EventDetailsScreen.swift

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ struct EventDetailsScreen: View {
99
@Environment(\.dismiss) private var dismiss
1010
@Environment(\.isNetworkConnected) private var isNetworkConnected
1111
@EnvironmentObject private var defaults: DefaultsService
12+
@StateObject private var calendarManager = CalendarManager()
1213
@State private var navigationDestination: NavigationDestination?
1314
@State private var sheetItem: SheetItem?
1415
@State private var isLoading = false
@@ -17,15 +18,15 @@ struct EventDetailsScreen: View {
1718
@State private var deleteCommentTask: Task<Void, Never>?
1819
@State private var deletePhotoTask: Task<Void, Never>?
1920
@State private var deleteEventTask: Task<Void, Never>?
20-
@State private var refreshButtonTask: Task<Void, Never>?
21+
@State private var refreshEventTask: Task<Void, Never>?
2122
@State var event: EventResponse
2223
let onDeletion: (Int) -> Void
2324

2425
var body: some View {
2526
ScrollView {
2627
VStack(spacing: 16) {
2728
headerAndMapSection
28-
if showParticipantSection {
29+
if defaults.isAuthorized {
2930
participantsSection
3031
}
3132
if event.hasPhotos {
@@ -162,10 +163,32 @@ private extension EventDetailsScreen {
162163
address: event.fullAddress ?? shortAddress,
163164
appleMapsURL: event.park.appleMapsURL
164165
)
166+
addToCalendarButton
165167
}
166168
.insideCardBackground()
167169
}
168170

171+
var addToCalendarButton: some View {
172+
Button("Добавить в календарь", action: calendarManager.requestAccess)
173+
.buttonStyle(SWButtonStyle(mode: .tinted, size: .large))
174+
.padding(.top, 12)
175+
.sheet(isPresented: $calendarManager.showCalendar) {
176+
EKEventEditViewControllerRepresentable(
177+
eventStore: calendarManager.eventStore,
178+
event: event
179+
)
180+
}
181+
.alert(
182+
"Необходимо разрешить полный доступ к календарю в настройках телефона",
183+
isPresented: $calendarManager.showSettingsAlert
184+
) {
185+
Button("Отмена", role: .cancel) {}
186+
Button("Перейти") {
187+
URLOpener.open(URL(string: UIApplication.openSettingsURLString))
188+
}
189+
}
190+
}
191+
169192
var descriptionSection: some View {
170193
SectionView(headerWithPadding: "Описание", mode: .card(padding: 12)) {
171194
Text(.init(event.formattedDescription))
@@ -191,7 +214,7 @@ private extension EventDetailsScreen {
191214
)
192215
}
193216
}
194-
if event.isCurrent ?? false {
217+
if event.isCurrent == true {
195218
FormRowView(
196219
title: "Пойду на мероприятие",
197220
trailingContent: .toggle(
@@ -334,7 +357,7 @@ private extension EventDetailsScreen {
334357

335358
func refreshAction() {
336359
sheetItem = nil
337-
refreshButtonTask = Task { await askForInfo(refresh: true) }
360+
refreshEventTask = Task { await askForInfo(refresh: true) }
338361
}
339362

340363
func askForInfo(refresh: Bool = false) async {
@@ -406,17 +429,9 @@ private extension EventDetailsScreen {
406429
: false
407430
}
408431

409-
var showParticipantSection: Bool {
410-
if defaults.isAuthorized {
411-
event.hasParticipants || event.isCurrent ?? false
412-
} else {
413-
false
414-
}
415-
}
416-
417432
func cancelTasks() {
418433
[
419-
refreshButtonTask,
434+
refreshEventTask,
420435
deleteCommentTask,
421436
goingToEventTask,
422437
deletePhotoTask,

0 commit comments

Comments
 (0)