Skip to content

Commit 768592c

Browse files
authored
Merge pull request #37 from OlegEremenko991/develop/updateMapScreen
Доработки карты: - Добавил возможность сменить вариант отображения площадок на главной (карта, список) - Запрещаю снимать все фильтры площадок, чтобы список был не пустым - Обновил тесты: добавил скриншот списка площадок - При авторизации устанавливаем фильтр по городу пользователя, при логауте - сбрасываем этот фильтр - Поднял версию билда до 8
2 parents d17cb41 + 239f002 commit 768592c

34 files changed

+318
-165
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,9 @@
4343
2. Настройки для генерации скриншотов находятся в файле [Snapfile](Snapfile) ([документация](https://docs.fastlane.tools/actions/snapshot/))
4444
3. Готовые скриншоты сохраняются в папке [screenshots/ru](./screenshots/ru)
4545

46-
| Профиль | Площадка | Прошедшие мероприятия | Мероприятие |
47-
| --- | --- | --- | --- |
48-
| <img src="./screenshots/ru/iPhone 13 Pro Max-1-profile.png"> | <img src="./screenshots/ru/iPhone 13 Pro Max-2-sportsGroundDetails.png"> | <img src="./screenshots/ru/iPhone 13 Pro Max-3-pastEvents.png"> | <img src="./screenshots/ru/iPhone 13 Pro Max-4-eventDetails.png"> |
46+
| Список площадок | Площадка | Прошедшие мероприятия | Мероприятие | Профиль |
47+
| --- | --- | --- | --- | --- |
48+
| <img src="./screenshots/ru/iPhone 13 Pro Max-1-sportsGroundsList.png"> | <img src="./screenshots/ru/iPhone 13 Pro Max-2-sportsGroundDetails.png"> | <img src="./screenshots/ru/iPhone 13 Pro Max-3-pastEvents.png"> | <img src="./screenshots/ru/iPhone 13 Pro Max-4-eventDetails.png"> | <img src="./screenshots/ru/iPhone 13 Pro Max-5-profile.png"> |
4949

5050
#### Модели девайсов, используемые для скриншотов
5151
- 6.5 дюйма: iPhone 13 Pro Max

SwiftUI-WorkoutApp.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1343,7 +1343,7 @@
13431343
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
13441344
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
13451345
CODE_SIGN_STYLE = Automatic;
1346-
CURRENT_PROJECT_VERSION = 7;
1346+
CURRENT_PROJECT_VERSION = 8;
13471347
DEVELOPMENT_ASSET_PATHS = "SwiftUI-WorkoutApp/Preview\\ Content/PreviewContent.swift SwiftUI-WorkoutApp/Preview\\ Content";
13481348
DEVELOPMENT_TEAM = CR68PP2Z3F;
13491349
ENABLE_PREVIEWS = YES;
@@ -1379,7 +1379,7 @@
13791379
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
13801380
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
13811381
CODE_SIGN_STYLE = Automatic;
1382-
CURRENT_PROJECT_VERSION = 7;
1382+
CURRENT_PROJECT_VERSION = 8;
13831383
DEVELOPMENT_ASSET_PATHS = "SwiftUI-WorkoutApp/Preview\\ Content/PreviewContent.swift SwiftUI-WorkoutApp/Preview\\ Content";
13841384
DEVELOPMENT_TEAM = CR68PP2Z3F;
13851385
ENABLE_PREVIEWS = YES;

SwiftUI-WorkoutApp/Models/EventResult.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ import Foundation
22

33
/// Результат создания/сохранения мероприятия
44
struct EventResult: Codable, Equatable {
5-
#warning("TODO: когда на бэке поправят формат данных в ответе по полю area_id, заменить эту модель на EventResponse")
5+
#warning("Бэк присылает неправильный формат данных в ответе по полю area_id, иначе заменил бы эту модель на EventResponse")
66
let id: Int
77
}

SwiftUI-WorkoutApp/Models/SportsGroundFilter.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Foundation
22

33
struct SportsGroundFilter: Equatable {
44
var size = SportsGroundSize.allCases
5-
var type = SportsGroundGrade.allCases
5+
var grade = SportsGroundGrade.allCases
66
var onlyMyCity = true
7+
var currentCity: String?
78
}

SwiftUI-WorkoutApp/Models/SportsGroundResult.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ import Foundation
22

33
/// Результат создания/сохранения площадки
44
struct SportsGroundResult: Codable, Equatable {
5-
#warning("TODO: когда на бэке поправят формат данных в ответе по полям city_id, type_id, class_id, заменить эту модель на SportsGround")
5+
#warning("Бэк присылает неправильный формат данных в ответе по полям city_id, country_id, type_id, class_id, иначе заменил бы эту модель на SportsGround")
66
let id: Int
77
}

SwiftUI-WorkoutApp/Screens/SportsGrounds/Filter/SportsGroundFilterView.swift

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,22 @@ struct SportsGroundFilterView: View {
99
ContentInSheet(title: "Фильтр площадок", spacing: .zero) {
1010
Form {
1111
Section("Размер") {
12-
ForEach(defaultFilter.size, id: \.self) { size in
13-
buttonFor(size)
12+
ForEach(defaultFilter.size, id: \.self) { groundSize in
13+
buttonFor(groundSize)
1414
}
1515
}
1616
Section("Тип") {
17-
ForEach(defaultFilter.type, id: \.self) { type in
18-
buttonFor(type)
17+
ForEach(defaultFilter.grade, id: \.self) { groundGrade in
18+
buttonFor(groundGrade)
1919
}
2020
}
21-
if defaults.isAuthorized, let cityName = cityName {
21+
if defaults.isAuthorized {
2222
Section {
2323
buttonForMyCity
2424
} header: {
2525
Text("Расположение")
2626
} footer: {
27-
Text("Твой город в профиле: \(cityName)")
27+
Text(footerCityText)
2828
}
2929
}
3030
resetFilterButton
@@ -37,6 +37,7 @@ private extension SportsGroundFilterView {
3737
func buttonFor(_ size: SportsGroundSize) -> some View {
3838
Button {
3939
if filter.size.contains(size) {
40+
guard filter.size.count > 1 else { return }
4041
filter.size = filter.size.filter { $0 != size }
4142
} else {
4243
filter.size.append(size)
@@ -49,17 +50,18 @@ private extension SportsGroundFilterView {
4950
}
5051
}
5152

52-
func buttonFor(_ type: SportsGroundGrade) -> some View {
53+
func buttonFor(_ grade: SportsGroundGrade) -> some View {
5354
Button {
54-
if filter.type.contains(type) {
55-
filter.type = filter.type.filter { $0 != type }
55+
if filter.grade.contains(grade) {
56+
guard filter.grade.count > 1 else { return }
57+
filter.grade = filter.grade.filter { $0 != grade }
5658
} else {
57-
filter.type.append(type)
59+
filter.grade.append(grade)
5860
}
5961
} label: {
6062
TextWithCheckmark(
61-
title: SportsGroundGrade(id: type.code).rawValue,
62-
showMark: filter.type.contains(type)
63+
title: SportsGroundGrade(id: grade.code).rawValue,
64+
showMark: filter.grade.contains(grade)
6365
)
6466
}
6567
}
@@ -82,14 +84,23 @@ private extension SportsGroundFilterView {
8284
.disabled(!canResetFilter)
8385
}
8486

85-
var cityName: String? {
86-
try? ShortAddressService().cityName(with: defaults.mainUserCityID, in: defaults.mainUserCountryID)
87+
var footerCityText: String {
88+
var resultString = ""
89+
let currentCity = filter.currentCity
90+
let userProfileCity = try? ShortAddressService().cityName(with: defaults.mainUserCityID, in: defaults.mainUserCountryID)
91+
if let userProfileCity {
92+
resultString += "Твой город в профиле: \(userProfileCity)"
93+
}
94+
if let currentCity, currentCity != userProfileCity {
95+
resultString += "\nТекущий город: \(currentCity)"
96+
}
97+
return resultString
8798
}
8899

89100
var canResetFilter: Bool {
90101
let isGroundFilterDifferent =
91-
filter.size != defaultFilter.size
92-
|| filter.type != defaultFilter.type
102+
filter.size.count != defaultFilter.size.count
103+
|| filter.grade.count != defaultFilter.grade.count
93104
let isCityFilterDifferent = defaults.isAuthorized
94105
? filter.onlyMyCity != defaultFilter.onlyMyCity
95106
: false

SwiftUI-WorkoutApp/Screens/SportsGrounds/Form/SportsGroundFormView.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ struct SportsGroundFormView: View {
2020
case let .createNew(address, coordinate, cityID):
2121
_viewModel = StateObject(
2222
wrappedValue: .init(
23-
address.wrappedValue,
24-
coordinate.wrappedValue.latitude,
25-
coordinate.wrappedValue.longitude,
23+
address,
24+
coordinate.latitude,
25+
coordinate.longitude,
2626
cityID
2727
)
2828
)
@@ -75,8 +75,8 @@ struct SportsGroundFormView: View {
7575
extension SportsGroundFormView {
7676
enum Mode {
7777
case createNew(
78-
address: Binding<String>,
79-
coordinate: Binding<CLLocationCoordinate2D>,
78+
address: String,
79+
coordinate: CLLocationCoordinate2D,
8080
cityID: Int
8181
)
8282
case editExisting(SportsGround)

SwiftUI-WorkoutApp/Screens/SportsGrounds/Map/MapViewUI.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,28 @@ struct MapViewUI: UIViewRepresentable {
55
/// Уникальный идентификатор карты, чтобы не плодить дубли
66
private let viewKey: String
77
private let region: MKCoordinateRegion
8+
private let ignoreUserLocation: Bool
89
private let annotations: [SportsGround]
910
@Binding private var needUpdateAnnotations: Bool
1011
@Binding private var needUpdateRegion: Bool
11-
@Binding private var ignoreUserLocation: Bool
1212
private static var mapViewStore = [String: MKMapView]()
1313
let openSelected: (SportsGround) -> Void
1414

1515
init(
1616
_ key: String,
1717
_ region: MKCoordinateRegion,
18+
_ ignoreUserLocation: Bool,
1819
_ pins: [SportsGround],
1920
_ needUpdatePins: Binding<Bool>,
2021
_ needUpdateRegion: Binding<Bool>,
21-
_ ignoreUserLocation: Binding<Bool>,
2222
openDetailsClbk: @escaping (SportsGround) -> Void
2323
) {
2424
self.viewKey = key
2525
self.region = region
26+
self.ignoreUserLocation = ignoreUserLocation
2627
self.annotations = pins
2728
self._needUpdateAnnotations = needUpdatePins
2829
self._needUpdateRegion = needUpdateRegion
29-
self._ignoreUserLocation = ignoreUserLocation
3030
self.openSelected = openDetailsClbk
3131
}
3232

SwiftUI-WorkoutApp/Screens/SportsGrounds/Map/SportsGroundsMapView.swift

Lines changed: 86 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ struct SportsGroundsMapView: View {
55
@EnvironmentObject private var network: CheckNetworkService
66
@EnvironmentObject private var defaults: DefaultsService
77
@StateObject private var viewModel = SportsGroundsMapViewModel()
8+
@State private var presentation = Presentation.map
89
@State private var needUpdateRecent = false
910
@State private var showErrorAlert = false
1011
@State private var alertMessage = ""
@@ -14,39 +15,25 @@ struct SportsGroundsMapView: View {
1415

1516
var body: some View {
1617
NavigationView {
17-
ZStack {
18-
MapViewUI(
19-
"SportsGroundsMapView",
20-
viewModel.region,
21-
viewModel.sportsGrounds,
22-
$viewModel.needUpdateAnnotations,
23-
$viewModel.needUpdateRegion,
24-
$viewModel.ignoreUserLocation,
25-
openDetailsClbk: openDetailsView
26-
)
27-
.opacity(mapOpacity)
28-
.animation(.easeInOut, value: viewModel.isLoading)
29-
ProgressView()
30-
.opacity(viewModel.isLoading ? 1 : 0)
31-
}
32-
.overlay(alignment: viewModel.isRegionSet ? .bottom : .center) {
33-
NavigationLink(isActive: $showDetailsView) {
34-
SportsGroundDetailView(
35-
for: viewModel.selectedGround,
36-
onDeletion: updateDeleted
37-
)
38-
} label: { EmptyView() }
39-
locationSettingsReminder
18+
VStack {
19+
segmentedControl
20+
groundsContent
21+
.disabled(viewModel.isLoading)
22+
.animation(.easeInOut, value: viewModel.isLoading)
23+
.overlay {
24+
ProgressView()
25+
.opacity(viewModel.isLoading ? 1 : 0)
26+
}
4027
}
4128
.onChange(of: viewModel.errorMessage, perform: setupErrorAlert)
42-
.onChange(of: defaults.mainUserCountryID, perform: updateFilterCountry)
29+
.onChange(of: defaults.mainUserInfo, perform: updateFilterForUser)
4330
.alert(alertMessage, isPresented: $showErrorAlert) {
4431
Button("Ok", action: closeAlert)
4532
}
4633
.task { await askForGrounds() }
4734
.onAppear {
4835
viewModel.onAppearAction()
49-
updateFilterCountry(countryID: defaults.mainUserCountryID)
36+
updateFilterForUser(info: defaults.mainUserInfo)
5037
}
5138
.onDisappear { viewModel.onDisappearAction() }
5239
.toolbar {
@@ -61,14 +48,20 @@ struct SportsGroundsMapView: View {
6148
rightBarButton
6249
}
6350
}
64-
.navigationTitle("Площадки")
65-
.navigationBarTitleDisplayMode(needToHideMap ? .large : .inline)
51+
.navigationTitle("Площадки (\(viewModel.sportsGrounds.count))")
52+
.navigationBarTitleDisplayMode(navigationTitleDisplayMode)
6653
}
6754
.navigationViewStyle(.stack)
6855
}
6956
}
7057

7158
private extension SportsGroundsMapView {
59+
/// Вариант отображения площадок на экране
60+
enum Presentation: String, CaseIterable, Equatable {
61+
case map = "Карта"
62+
case list = "Список"
63+
}
64+
7265
var filterButton: some View {
7366
Button {
7467
showFilters.toggle()
@@ -86,19 +79,70 @@ private extension SportsGroundsMapView {
8679
}
8780
}
8881

82+
var segmentedControl: some View {
83+
Picker("Способ отображения", selection: $presentation) {
84+
ForEach(Presentation.allCases, id: \.self) {
85+
Text($0.rawValue)
86+
.accessibilityIdentifier($0.rawValue)
87+
}
88+
}
89+
.pickerStyle(.segmented)
90+
.padding(.horizontal)
91+
}
92+
93+
@ViewBuilder
94+
var groundsContent: some View {
95+
switch presentation {
96+
case .list:
97+
List(viewModel.sportsGrounds) { ground in
98+
NavigationLink {
99+
SportsGroundDetailView(
100+
for: ground,
101+
onDeletion: updateDeleted
102+
)
103+
} label: {
104+
SportsGroundViewCell(model: ground)
105+
}
106+
.accessibilityIdentifier("SportsGroundViewCell")
107+
}
108+
.opacity(viewModel.isLoading ? 0.5 : 1)
109+
case .map:
110+
MapViewUI(
111+
"SportsGroundsMapView",
112+
viewModel.region,
113+
viewModel.ignoreUserLocation,
114+
viewModel.sportsGrounds,
115+
$viewModel.needUpdateAnnotations,
116+
$viewModel.needUpdateRegion,
117+
openDetailsClbk: openDetailsView
118+
)
119+
.opacity(mapOpacity)
120+
.overlay(alignment: viewModel.isRegionSet ? .bottom : .center) {
121+
NavigationLink(isActive: $showDetailsView) {
122+
SportsGroundDetailView(
123+
for: viewModel.selectedGround,
124+
onDeletion: updateDeleted
125+
)
126+
} label: { EmptyView() }
127+
locationSettingsReminder
128+
}
129+
}
130+
}
131+
132+
var navigationTitleDisplayMode: NavigationBarItem.TitleDisplayMode {
133+
switch presentation {
134+
case .list: return .inline
135+
case .map: return needToHideMap ? .large : .inline
136+
}
137+
}
138+
89139
var needToHideMap: Bool {
90140
!viewModel.isRegionSet && viewModel.ignoreUserLocation
91141
}
92142

93143
var mapOpacity: Double {
94-
if needToHideMap {
95-
return .zero
96-
}
97-
if viewModel.isLoading {
98-
return 0.5
99-
} else {
100-
return 1
101-
}
144+
guard !needToHideMap else { return .zero }
145+
return viewModel.isLoading ? 0.5 : 1
102146
}
103147

104148
var isLeftToolbarPartDisabled: Bool {
@@ -150,8 +194,8 @@ private extension SportsGroundsMapView {
150194
ContentInSheet(title: "Новая площадка", spacing: .zero) {
151195
SportsGroundFormView(
152196
.createNew(
153-
address: $viewModel.addressString,
154-
coordinate: $viewModel.region.center,
197+
address: viewModel.addressString,
198+
coordinate: viewModel.region.center,
155199
cityID: defaults.mainUserCityID
156200
),
157201
refreshClbk: updateRecent
@@ -173,10 +217,13 @@ private extension SportsGroundsMapView {
173217
}
174218

175219
func updateDeleted(groundID: Int) {
176-
viewModel.deleteSportsGroundFromList()
220+
viewModel.deleteSportsGroundFromList(with: groundID)
177221
}
178222

179-
func updateFilterCountry(countryID: Int) {
223+
/// Обновляем фильтр
224+
///
225+
/// Параметр не используем, т.к. передаем `defaults` во вьюмодель
226+
func updateFilterForUser(info: UserResponse?) {
180227
viewModel.updateFilter(with: defaults)
181228
}
182229

0 commit comments

Comments
 (0)