Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
22 changes: 16 additions & 6 deletions SwiftUI-WorkoutApp.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -106,6 +108,8 @@
671B4AE82D4F623100286996 /* ModernPickedImagesGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModernPickedImagesGrid.swift; sourceTree = "<group>"; };
671B4AEA2D4F683E00286996 /* ImagePickerViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerViews.swift; sourceTree = "<group>"; };
671D7DEB28210D2F0068E728 /* EmptyContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyContentView.swift; sourceTree = "<group>"; };
671F17FC2D589E7E005EE522 /* CalendarManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarManager.swift; sourceTree = "<group>"; };
671F17FE2D589F45005EE522 /* EKEventEditViewControllerRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EKEventEditViewControllerRepresentable.swift; sourceTree = "<group>"; };
6740003F2D55E97900E5CB06 /* BlackListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlackListScreen.swift; sourceTree = "<group>"; };
67419ACE282E70B9004F5339 /* ParksListScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParksListScreen.swift; sourceTree = "<group>"; };
6747575528113419002F0A24 /* ChangePasswordScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangePasswordScreen.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -217,6 +221,8 @@
6798AA52280AF43900DB76F1 /* EventsListScreen.swift */,
67FBF64E28338A2E008A7968 /* EventDetailsScreen.swift */,
675EC65E2815532800C2E229 /* EventFormScreen.swift */,
671F17FC2D589E7E005EE522 /* CalendarManager.swift */,
671F17FE2D589F45005EE522 /* EKEventEditViewControllerRepresentable.swift */,
);
path = Events;
sourceTree = "<group>";
Expand Down Expand Up @@ -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 */,
Expand All @@ -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 */,
Expand Down Expand Up @@ -868,16 +876,17 @@
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;
GENERATE_INFOPLIST_FILE = YES;
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;
Expand All @@ -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";
Expand Down Expand Up @@ -919,16 +928,17 @@
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;
GENERATE_INFOPLIST_FILE = YES;
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;
Expand All @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down Expand Up @@ -147,6 +148,10 @@ public extension EventResponse {
previewImageStringURL.queryAllowedURL
}

var eventBeginDateForCalendar: Date {
DateFormatterService.dateFromIsoString(beginDate)
}

var eventDateString: String {
DateFormatterService.readableDate(from: beginDate)
}
Expand Down
18 changes: 18 additions & 0 deletions SwiftUI-WorkoutApp/Resources/InfoPlist.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 20 additions & 0 deletions SwiftUI-WorkoutApp/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -1410,6 +1410,16 @@
}
}
},
"Добавить в календарь" : {
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Add to calendar"
}
}
}
},
"Добавить комментарий" : {
"localizations" : {
"en" : {
Expand Down Expand Up @@ -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" : {
Expand Down
25 changes: 25 additions & 0 deletions SwiftUI-WorkoutApp/Screens/Events/CalendarManager.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
41 changes: 28 additions & 13 deletions SwiftUI-WorkoutApp/Screens/Events/EventDetailsScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -17,15 +18,15 @@ struct EventDetailsScreen: View {
@State private var deleteCommentTask: Task<Void, Never>?
@State private var deletePhotoTask: Task<Void, Never>?
@State private var deleteEventTask: Task<Void, Never>?
@State private var refreshButtonTask: Task<Void, Never>?
@State private var refreshEventTask: Task<Void, Never>?
@State var event: EventResponse
let onDeletion: (Int) -> Void

var body: some View {
ScrollView {
VStack(spacing: 16) {
headerAndMapSection
if showParticipantSection {
if defaults.isAuthorized {
participantsSection
}
if event.hasPhotos {
Expand Down Expand Up @@ -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))
Expand All @@ -191,7 +214,7 @@ private extension EventDetailsScreen {
)
}
}
if event.isCurrent ?? false {
if event.isCurrent == true {
FormRowView(
title: "Пойду на мероприятие",
trailingContent: .toggle(
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading