Skip to content

Instantly share code, notes, and snippets.

@maiyama18
Last active April 19, 2024 14:46
Show Gist options
  • Save maiyama18/2ff91be84b3998d90f7465ae1574550b to your computer and use it in GitHub Desktop.
Save maiyama18/2ff91be84b3998d90f7465ae1574550b to your computer and use it in GitHub Desktop.
import SwiftUI
import OSLog
struct LogScreen: View {
enum SearchScope: Hashable {
case all
case category(LogCategory)
}
@State private var allLogEntries: [LogEntry] = []
@State private var searchScope: SearchScope = .all
@State private var query: String = ""
@State private var loading: Bool = false
private let logStore = LogStore()
private var visibleEntries: [LogEntry] {
let scopedEntries: [LogEntry]
switch searchScope {
case .all:
scopedEntries = allLogEntries
case .category(let category):
scopedEntries = allLogEntries.filter { $0.category == category }
}
let trimmedQuery = query.trimmingCharacters(in: .whitespaces)
return trimmedQuery.isEmpty ? scopedEntries : scopedEntries.filter { $0.message.range(of: trimmedQuery, options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive]) != nil }
}
var body: some View {
Group {
if loading {
ProgressView()
} else {
VStack(alignment: .leading, spacing: 0) {
Picker(selection: $searchScope) {
Text("all").tag(SearchScope.all)
ForEach(LogCategory.allCases, id: \.self) { category in
Text(category.rawValue).tag(SearchScope.category(category))
}
} label: {
Text("Category")
}
.padding(.horizontal, 8)
List {
ForEach(visibleEntries, id: \.date) { entry in
LogRowView(entry: entry)
.listRowInsets(.init(top: 0, leading: 0, bottom: 0, trailing: 0))
}
}
.listStyle(.plain)
}
.searchable(text: $query)
}
}
.task {
guard allLogEntries.isEmpty else { return }
loading = true
defer { loading = false }
do {
allLogEntries = try await logStore.getAllLogEntries()
} catch {
print(error)
}
}
.navigationTitle("Log")
}
}
struct LogRowView: View {
private static let logDateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "MM-dd HH:mm:ss.SSS"
formatter.calendar = Calendar(identifier: .gregorian)
formatter.locale = Locale(identifier: "en_US")
return formatter
}()
var entry: LogEntry
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack {
Image(systemName: "circle.fill")
.foregroundColor(entry.level.color)
.font(.caption2)
Text(Self.logDateFormatter.string(from: entry.date))
Text(entry.category.rawValue)
.foregroundStyle(.white)
.padding(.vertical, 2)
.padding(.horizontal, 4)
.background(entry.category.color)
.cornerRadius(4)
.bold()
}
Text(entry.message)
.lineLimit(5)
}
.font(.caption.monospaced())
.padding(.vertical, 8)
.padding(.horizontal, 16)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
extension LogCategory {
var color: Color {
switch self {
case .app:
.indigo
case .iCloud:
.red
case .feed:
.green
case .settings:
.orange
}
}
}
extension OSLogEntryLog.Level {
var color: Color {
switch self {
case .undefined:
.clear
case .debug:
.clear
case .info:
.clear
case .notice:
.gray
case .error:
.yellow
case .fault:
.red
@unknown default:
.clear
}
}
}
import OSLog
enum LogCategory: String, CaseIterable, Sendable {
case app
case iCloud
case feed
case settings
}
struct LogEntry: Sendable {
var date: Date
var category: LogCategory
var level: OSLogEntryLog.Level
var message: String
}
actor LogStore {
func getAllLogEntries() throws -> [LogEntry] {
let store = try OSLogStore(scope: .currentProcessIdentifier)
let position = store.position(timeIntervalSinceLatestBoot: 1)
return try store.getEntries(at: position)
.compactMap { $0 as? OSLogEntryLog }
.filter { $0.subsystem == loggerSubsystem() }
.compactMap {
guard let category = LogCategory(rawValue: $0.category) else { return nil }
return LogEntry(
date: $0.date,
category: category,
level: $0.level,
message: $0.composedMessage
)
}
.sorted(by: { $0.date > $1.date })
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment