Skip to content

Commit c1e41fc

Browse files
authored
Экран с логами (#215)
Сделал экран с логами в настройках
1 parent 0c00ecd commit c1e41fc

File tree

4 files changed

+284
-16
lines changed

4 files changed

+284
-16
lines changed

SwiftUI-WorkoutApp.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
674D0623282A9896007E75C6 /* SearchUsersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 674D0622282A9896007E75C6 /* SearchUsersView.swift */; };
2424
674DF03E2B11254D00828016 /* Binding+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 674DF03D2B11254D00828016 /* Binding+.swift */; };
2525
674DF0402B11257D00828016 /* NavigationLink+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 674DF03F2B11257D00828016 /* NavigationLink+.swift */; };
26+
674E704E2B24D382008AE9D0 /* LoggerScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 674E704D2B24D382008AE9D0 /* LoggerScreen.swift */; };
2627
67515699283FEC3100501346 /* PickedImagesGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67515698283FEC3100501346 /* PickedImagesGrid.swift */; };
2728
67551C362AEC338600084A35 /* SWAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67551C352AEC338600084A35 /* SWAddress.swift */; };
2829
6758463B2965B0F6000BA5E0 /* UIImage+Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6758463A2965B0F6000BA5E0 /* UIImage+Identifiable.swift */; };
@@ -115,6 +116,7 @@
115116
674D0622282A9896007E75C6 /* SearchUsersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchUsersView.swift; sourceTree = "<group>"; };
116117
674DF03D2B11254D00828016 /* Binding+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+.swift"; sourceTree = "<group>"; };
117118
674DF03F2B11257D00828016 /* NavigationLink+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NavigationLink+.swift"; sourceTree = "<group>"; };
119+
674E704D2B24D382008AE9D0 /* LoggerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerScreen.swift; sourceTree = "<group>"; };
118120
675083AA297C0A60008D8C52 /* WorkoutAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = WorkoutAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
119121
67515698283FEC3100501346 /* PickedImagesGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PickedImagesGrid.swift; sourceTree = "<group>"; };
120122
67551C352AEC338600084A35 /* SWAddress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SWAddress.swift; sourceTree = "<group>"; };
@@ -273,6 +275,7 @@
273275
67419AD8282E8E9E004F5339 /* Settings */ = {
274276
isa = PBXGroup;
275277
children = (
278+
674E704F2B24F032008AE9D0 /* Logger */,
276279
670CA19D280E8F09003914A3 /* SettingsView.swift */,
277280
6798AA72280B43FE00DB76F1 /* LoginView.swift */,
278281
);
@@ -299,6 +302,14 @@
299302
path = SportsGrounds;
300303
sourceTree = "<group>";
301304
};
305+
674E704F2B24F032008AE9D0 /* Logger */ = {
306+
isa = PBXGroup;
307+
children = (
308+
674E704D2B24D382008AE9D0 /* LoggerScreen.swift */,
309+
);
310+
path = Logger;
311+
sourceTree = "<group>";
312+
};
302313
67515697283FEC0B00501346 /* ImagePicker */ = {
303314
isa = PBXGroup;
304315
children = (
@@ -782,6 +793,7 @@
782793
679F3B03296841DD00BB3590 /* URLOpener.swift in Sources */,
783794
67BAF3F6283620ED00DB40D9 /* SportsGroundLocationInfo.swift in Sources */,
784795
67627755283A4C77009C203F /* JournalEntriesList.swift in Sources */,
796+
674E704E2B24D382008AE9D0 /* LoggerScreen.swift in Sources */,
785797
67B78716281D8006008B104F /* SportsGroundsMapViewModel.swift in Sources */,
786798
671D7DEC28210D2F0068E728 /* EmptyContentView.swift in Sources */,
787799
6773111A2965FFAA003CD13A /* PreviewContent.swift in Sources */,

SwiftUI-WorkoutApp/Resources/Localizable.xcstrings

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -981,6 +981,22 @@
981981
}
982982
}
983983
},
984+
"Восстановить пароль" : {
985+
"localizations" : {
986+
"en" : {
987+
"stringUnit" : {
988+
"state" : "translated",
989+
"value" : "Restore your password"
990+
}
991+
},
992+
"ru" : {
993+
"stringUnit" : {
994+
"state" : "translated",
995+
"value" : "Восстановить пароль"
996+
}
997+
}
998+
}
999+
},
9841000
"Все" : {
9851001
"comment" : "Уровень доступа - для всех",
9861002
"extractionState" : "manual",
@@ -1552,21 +1568,8 @@
15521568
}
15531569
}
15541570
},
1555-
"Восстановить пароль" : {
1556-
"localizations" : {
1557-
"en" : {
1558-
"stringUnit" : {
1559-
"state" : "translated",
1560-
"value" : "Restore your password"
1561-
}
1562-
},
1563-
"ru" : {
1564-
"stringUnit" : {
1565-
"state" : "translated",
1566-
"value" : "Восстановить пароль"
1567-
}
1568-
}
1569-
}
1571+
"Загружаем логи..." : {
1572+
15701573
},
15711574
"Закрыть" : {
15721575
"localizations" : {
@@ -1868,6 +1871,9 @@
18681871
}
18691872
}
18701873
}
1874+
},
1875+
"Категория" : {
1876+
18711877
},
18721878
"Когда" : {
18731879
"localizations" : {
@@ -1949,6 +1955,9 @@
19491955
}
19501956
}
19511957
}
1958+
},
1959+
"Логи" : {
1960+
19521961
},
19531962
"Логин" : {
19541963
"extractionState" : "manual",
@@ -1983,6 +1992,9 @@
19831992
}
19841993
}
19851994
}
1995+
},
1996+
"Логов пока нет" : {
1997+
19861998
},
19871999
"Магазин WORKOUT" : {
19882000
"extractionState" : "manual",
@@ -3320,6 +3332,9 @@
33203332
}
33213333
}
33223334
}
3335+
},
3336+
"С такими фильтрами логов нет" : {
3337+
33233338
},
33243339
"Сбросить фильтры" : {
33253340
"localizations" : {
@@ -3905,6 +3920,9 @@
39053920
}
39063921
}
39073922
}
3923+
},
3924+
"Уровень" : {
3925+
39083926
},
39093927
"Участники" : {
39103928
"extractionState" : "manual",
@@ -3939,6 +3957,9 @@
39393957
}
39403958
}
39413959
}
3960+
},
3961+
"Фильтр логов" : {
3962+
39423963
},
39433964
"Фильтр площадок" : {
39443965
"extractionState" : "manual",
@@ -4077,4 +4098,4 @@
40774098
}
40784099
},
40794100
"version" : "1.0"
4080-
}
4101+
}
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
#if DEBUG
2+
import Foundation
3+
import OSLog
4+
import SWDesignSystem
5+
import SwiftUI
6+
7+
private final class LogStore: ObservableObject {
8+
private static let logger = Logger(
9+
subsystem: Bundle.main.bundleIdentifier!,
10+
category: String(describing: LogStore.self)
11+
)
12+
13+
@Published private(set) var state = State.empty
14+
var logs: [State.LogModel] {
15+
switch state {
16+
case .empty, .loading: []
17+
case let .ready(array): array
18+
}
19+
}
20+
21+
/// Все категории, которые есть в логах
22+
@Published private(set) var categories = [String]()
23+
/// Все уровни, которые есть в логах
24+
@Published private(set) var levels = [State.LogModel.Level]()
25+
26+
func getLogs() async {
27+
await MainActor.run { state = .loading }
28+
do {
29+
let store = try OSLogStore(scope: .currentProcessIdentifier)
30+
let position = store.position(timeIntervalSinceLatestBoot: 1)
31+
let entries: [State.LogModel] = try store
32+
.getEntries(at: position)
33+
.compactMap { $0 as? OSLogEntryLog }
34+
.filter { $0.subsystem == Bundle.main.bundleIdentifier! }
35+
.map {
36+
.init(
37+
dateString: $0.date.formatted(date: .long, time: .standard),
38+
category: $0.category,
39+
level: .init(rawValue: $0.level.rawValue) ?? .undefined,
40+
message: $0.composedMessage
41+
)
42+
}
43+
await MainActor.run {
44+
categories = Array(Set(entries.map(\.category)))
45+
levels = Array(Set(entries.map(\.level)))
46+
state = .ready(entries)
47+
}
48+
} catch {
49+
Self.logger.warning("\(error.localizedDescription, privacy: .public)")
50+
await MainActor.run { state = .empty }
51+
}
52+
}
53+
54+
enum State: Equatable {
55+
case empty, loading, ready([LogModel])
56+
57+
var isLoading: Bool { self == .loading }
58+
59+
struct LogModel: Identifiable, Equatable {
60+
let id = UUID()
61+
let dateString: String
62+
let category: String
63+
let level: Level
64+
let message: String
65+
66+
enum Level: Int, CaseIterable {
67+
case undefined = 0
68+
case debug = 1
69+
case info = 2
70+
case notice = 3
71+
case error = 4
72+
case fault = 5
73+
74+
var emoji: String {
75+
switch self {
76+
case .undefined: "🤨"
77+
case .debug: "🛠️"
78+
case .info: "ℹ️"
79+
case .notice: "💁‍♂️"
80+
case .error: "⚠️"
81+
case .fault: "⛔️"
82+
}
83+
}
84+
}
85+
}
86+
}
87+
}
88+
89+
struct LoggerScreen: View {
90+
@StateObject private var logStore = LogStore()
91+
@State private var categoriesToShow = [String]()
92+
@State private var levelsToShow = [LogStore.State.LogModel.Level]()
93+
@State private var showFilter = false
94+
private var isFilterOn: Bool {
95+
!categoriesToShow.isEmpty || !levelsToShow.isEmpty
96+
}
97+
98+
private var filteredLogs: [LogStore.State.LogModel] {
99+
if isFilterOn {
100+
let filterCategories = !categoriesToShow.isEmpty
101+
let filterLevels = !levelsToShow.isEmpty
102+
return logStore.logs.filter { log in
103+
let hasCategory = filterCategories
104+
? categoriesToShow.contains(log.category)
105+
: true
106+
let hasLevel = filterLevels
107+
? levelsToShow.contains(log.level)
108+
: true
109+
return hasCategory && hasLevel
110+
}
111+
} else {
112+
return logStore.logs
113+
}
114+
}
115+
116+
var body: some View {
117+
ZStack {
118+
switch logStore.state {
119+
case .empty:
120+
Text("Логов пока нет")
121+
case .loading:
122+
Text("Загружаем логи...")
123+
case .ready:
124+
if filteredLogs.isEmpty {
125+
Text("С такими фильтрами логов нет")
126+
} else {
127+
ScrollView {
128+
LazyVStack(alignment: .leading, spacing: 16) {
129+
ForEach(Array(zip(filteredLogs.indices, filteredLogs)), id: \.0) { index, log in
130+
VStack(alignment: .leading, spacing: 8) {
131+
HStack(spacing: 8) {
132+
Text(log.level.emoji)
133+
Text(log.dateString)
134+
}
135+
Text(log.category).bold()
136+
Text(log.message)
137+
}
138+
.frame(maxWidth: .infinity, alignment: .leading)
139+
.multilineTextAlignment(.leading)
140+
.withDivider(if: index != filteredLogs.indices.last, spacing: 12)
141+
}
142+
}
143+
.padding([.top, .horizontal])
144+
}
145+
}
146+
}
147+
}
148+
.animation(.default, value: logStore.state)
149+
.loadingOverlay(if: logStore.state.isLoading)
150+
.frame(maxWidth: .infinity, maxHeight: .infinity)
151+
.navigationTitle("Логи")
152+
.background(Color.swBackground)
153+
.task { await logStore.getLogs() }
154+
.toolbar {
155+
ToolbarItem(placement: .topBarTrailing) {
156+
Button {
157+
showFilter = true
158+
} label: {
159+
Icons.Regular.filter.view
160+
.symbolVariant(isFilterOn ? .fill : .none)
161+
}
162+
.disabled(logStore.state.isLoading)
163+
}
164+
}
165+
.sheet(isPresented: $showFilter) {
166+
ContentInSheet(title: "Фильтр логов", spacing: 0) {
167+
filterView
168+
}
169+
}
170+
}
171+
172+
private var filterView: some View {
173+
ScrollView {
174+
VStack(spacing: 32) {
175+
SectionView(header: "Категория", mode: .card()) {
176+
VStack(spacing: 0) {
177+
ForEach(Array(zip(logStore.categories.indices, logStore.categories)), id: \.0) { index, category in
178+
Button {
179+
if categoriesToShow.contains(category) {
180+
categoriesToShow = categoriesToShow.filter { $0 != category }
181+
} else {
182+
categoriesToShow.append(category)
183+
}
184+
} label: {
185+
TextWithCheckmarkRowView(
186+
text: .init(category),
187+
isChecked: categoriesToShow.contains(category)
188+
)
189+
}
190+
.withDivider(if: index != logStore.categories.endIndex - 1)
191+
}
192+
}
193+
}
194+
SectionView(header: "Уровень", mode: .card()) {
195+
VStack(spacing: 0) {
196+
ForEach(Array(zip(logStore.levels.indices, logStore.levels)), id: \.0) { index, level in
197+
Button {
198+
if levelsToShow.contains(level) {
199+
levelsToShow = levelsToShow.filter { $0 != level }
200+
} else {
201+
levelsToShow.append(level)
202+
}
203+
} label: {
204+
TextWithCheckmarkRowView(
205+
text: .init(level.emoji),
206+
isChecked: levelsToShow.contains(level)
207+
)
208+
}
209+
.withDivider(if: index != logStore.levels.endIndex - 1)
210+
}
211+
}
212+
}
213+
Button("Сбросить фильтры") {
214+
categoriesToShow = []
215+
levelsToShow = []
216+
}
217+
.buttonStyle(SWButtonStyle(mode: .filled, size: .large))
218+
.disabled(!isFilterOn)
219+
}
220+
.padding([.top, .horizontal])
221+
}
222+
}
223+
}
224+
225+
#Preview { LoggerScreen() }
226+
#endif

0 commit comments

Comments
 (0)