listless

A simple list app for Apple platforms
Log | Files | Refs | README | LICENSE

commit b29c19ff482498ff27a52d52c2b3797495961379
parent edcc9fdb76d8a4b9dd19c97286a98953c16de071
Author: Michael Camilleri <[email protected]>
Date:   Tue,  3 Mar 2026 16:49:30 +0900

Add settings screen

This project originally avoided having a settings screen but it was only
possible to hold out for so long. The settings screen allows a user to
change the heading that appears above the list (now defaults to 'Items')
to arbitrary text. A user can also toggle dark mode. A settings screen
was also a natural place to put an about screen so that's been added,
too.

Co-Authored-By: Claude 4.6 Opus <[email protected]>

Diffstat:
MListless.xcodeproj/project.pbxproj | 12++++++++++++
MListlessMac/Listless.entitlements | 2--
MListlessiOS/Extensions/TaskListView+NavigationHeader.swift | 40+++++++++++++++++++++++++---------------
MListlessiOS/ListlessiOSApp.swift | 14++++++++++++++
AListlessiOS/Views/AboutView.swift | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AListlessiOS/Views/SettingsView.swift | 46++++++++++++++++++++++++++++++++++++++++++++++
AListlessiOS/Views/SyncDiagnosticsView.swift | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MListlessiOS/Views/TaskListView.swift | 127++++++++++++++++---------------------------------------------------------------
AMedia.xcassets/AboutIcon.imageset/Contents.json | 12++++++++++++
AMedia.xcassets/AboutIcon.imageset/Icon.png | 0
10 files changed, 295 insertions(+), 118 deletions(-)

diff --git a/Listless.xcodeproj/project.pbxproj b/Listless.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 060436CDDB388BC04C51581A /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3E995954787F0A14CCFF348 /* AboutView.swift */; }; + 072594B24D88AD6D1DF7AFE5 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C611E04943F1D82D6F975592 /* SettingsView.swift */; }; 0ACA67F6578EFF181EE5C9A7 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBB8A3BEB346267B30B4675F /* TaskItem.swift */; }; 0F12D56A528FCBF8A67864CB /* TappableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECE0E961F87BA32FA87BF90 /* TappableTextField.swift */; }; 11AA75BE98CFBE44AEAB7100 /* Media.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9404C09EE1A4D91DFF338464 /* Media.xcassets */; }; @@ -22,6 +24,7 @@ 3ABE52A15C2059D8D5570528 /* TaskStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC3DEE364304587D280C5672 /* TaskStore.swift */; }; 3D1F551A03B97ECF4E3DC8B0 /* TaskRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E06485DBE35B60868E14202A /* TaskRowView.swift */; }; 3EDB6A9A30B4226C15E7F44D /* AppCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = 46C46FC97E6DB6FF81AC5C22 /* AppCommands.swift */; }; + 4DD2030E321567BD25661760 /* SyncDiagnosticsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9288507CE6023425D1DE724 /* SyncDiagnosticsView.swift */; }; 4E5A0A02121E02124F80E320 /* TaskListView+SyncUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7BD42B1E3C71333FA24893 /* TaskListView+SyncUI.swift */; }; 5035EC4C7518A5FF9AD454CA /* TaskRowDragGesture.swift in Sources */ = {isa = PBXBuildFile; fileRef = F416DD868A4C044F0D64F8D0 /* TaskRowDragGesture.swift */; }; 5761B201BF46FCA9C5C98CEF /* PlatformScrollIndicatorsModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 466F9B0E407DF1F5B4789531 /* PlatformScrollIndicatorsModifier.swift */; }; @@ -126,11 +129,13 @@ B7588879D0FA1C2A8BCEF14F /* HoverCursorModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoverCursorModifier.swift; sourceTree = "<group>"; }; BFF7D84B54AE70036D205CA4 /* PullToCreate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullToCreate.swift; sourceTree = "<group>"; }; C14858BDFD1FD5119F1F24A6 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; }; + C611E04943F1D82D6F975592 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; }; C71466C5CD1A5BA984352F8D /* Listless iOS Unit Tests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = "Listless iOS Unit Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; C9B14DC786A336008AAB78EE /* .gitkeep */ = {isa = PBXFileReference; path = .gitkeep; sourceTree = "<group>"; }; CB43816B8E7F083A2AD07F28 /* TaskListView+Toolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+Toolbar.swift"; sourceTree = "<group>"; }; D2C018476BD91B73870244B9 /* TaskListViewProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListViewProtocol.swift; sourceTree = "<group>"; }; D2D9CDDA8913CD116FB4DA74 /* TaskListView+PullGestures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TaskListView+PullGestures.swift"; sourceTree = "<group>"; }; + D3E995954787F0A14CCFF348 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; }; D43D37CE25806380C0B13466 /* BuildNumber.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = BuildNumber.xcconfig; sourceTree = "<group>"; }; D57C3CC81C4380EFAE4DB910 /* TaskRowDragGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskRowDragGesture.swift; sourceTree = "<group>"; }; D640E7D21735C62A30A26DA4 /* ClickableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClickableTextField.swift; sourceTree = "<group>"; }; @@ -139,6 +144,7 @@ E2174C43654733E9D4023157 /* PlatformTextFieldWidthModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformTextFieldWidthModifier.swift; sourceTree = "<group>"; }; E51B8129962E5CC78ECDDC2B /* TaskListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskListView.swift; sourceTree = "<group>"; }; E8448C5778F75F52719114AF /* PlatformTextFieldWidthModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformTextFieldWidthModifier.swift; sourceTree = "<group>"; }; + E9288507CE6023425D1DE724 /* SyncDiagnosticsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncDiagnosticsView.swift; sourceTree = "<group>"; }; F1E998119283F784B9ADEE28 /* AppColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppColors.swift; sourceTree = "<group>"; }; F416DD868A4C044F0D64F8D0 /* TaskRowDragGesture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TaskRowDragGesture.swift; sourceTree = "<group>"; }; FA5D58EC1FBAA96E83A79445 /* PlatformScrollIndicatorsModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlatformScrollIndicatorsModifier.swift; sourceTree = "<group>"; }; @@ -207,8 +213,11 @@ 58F917D865E0BDF4EF282306 /* Views */ = { isa = PBXGroup; children = ( + D3E995954787F0A14CCFF348 /* AboutView.swift */, 567DBAC2A39FA2760D006AAB /* PullToClear.swift */, BFF7D84B54AE70036D205CA4 /* PullToCreate.swift */, + C611E04943F1D82D6F975592 /* SettingsView.swift */, + E9288507CE6023425D1DE724 /* SyncDiagnosticsView.swift */, E51B8129962E5CC78ECDDC2B /* TaskListView.swift */, 199CC2F58DD7CBA3F2229366 /* TaskRowView.swift */, ); @@ -505,6 +514,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 060436CDDB388BC04C51581A /* AboutView.swift in Sources */, BA6953E0EFE6F8255F05A3FD /* AccentColor.swift in Sources */, 5D3EE9526DA269EE9EE3AB52 /* AppColors.swift in Sources */, 3EDB6A9A30B4226C15E7F44D /* AppCommands.swift in Sources */, @@ -519,6 +529,8 @@ 77FE96F070B1F7FE31A9CE51 /* PlatformTextFieldWidthModifier.swift in Sources */, 785721EB774EAC6BBA26C038 /* PullToClear.swift in Sources */, E47136CA7428927395D8C7C7 /* PullToCreate.swift in Sources */, + 072594B24D88AD6D1DF7AFE5 /* SettingsView.swift in Sources */, + 4DD2030E321567BD25661760 /* SyncDiagnosticsView.swift in Sources */, 0F12D56A528FCBF8A67864CB /* TappableTextField.swift in Sources */, 0ACA67F6578EFF181EE5C9A7 /* TaskItem.swift in Sources */, 1477B460E3DEFA3FDB7DA65B /* TaskListTypes.swift in Sources */, diff --git a/ListlessMac/Listless.entitlements b/ListlessMac/Listless.entitlements @@ -2,8 +2,6 @@ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> - <key>com.apple.security.app-sandbox</key> - <true/> <key>aps-environment</key> <string>development</string> <key>com.apple.developer.icloud-container-identifiers</key> diff --git a/ListlessiOS/Extensions/TaskListView+NavigationHeader.swift b/ListlessiOS/Extensions/TaskListView+NavigationHeader.swift @@ -2,21 +2,31 @@ import SwiftUI extension TaskListView { var navigationHeader: some View { - Text("Tasks") - .font(.largeTitle) - .fontWeight(.bold) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.horizontal, 16) - .padding(.bottom, 8) - .contentShape(Rectangle()) - .onTapGesture { - selectedTaskID = nil - focusedField = .scrollView + HStack { + Text(headingText) + .font(.largeTitle) + .fontWeight(.bold) + Spacer() + Button { + showSettings() + } label: { + Image(systemName: "gearshape") + .font(.title2) + .foregroundStyle(.secondary) } - .simultaneousGesture( - TapGesture(count: 4).onEnded { - showSyncDiagnostics() - } - ) + .buttonStyle(.plain) + } + .padding(.horizontal, 16) + .padding(.bottom, 8) + .contentShape(Rectangle()) + .onTapGesture { + selectedTaskID = nil + focusedField = .scrollView + } + .simultaneousGesture( + TapGesture(count: 4).onEnded { + showSyncDiagnostics() + } + ) } } diff --git a/ListlessiOS/ListlessiOSApp.swift b/ListlessiOS/ListlessiOSApp.swift @@ -2,6 +2,7 @@ import SwiftUI @main struct ListlessiOSApp: App { + @AppStorage("appearanceMode") private var appearanceMode = 0 private let persistenceController: PersistenceController init() { @@ -19,6 +20,19 @@ struct ListlessiOSApp: App { Color.clear.frame(height: 8) } .environment(\.managedObjectContext, persistenceController.viewContext) + .onChange(of: appearanceMode, initial: true) { _, newValue in + let style: UIUserInterfaceStyle = switch newValue { + case 1: .light + case 2: .dark + default: .unspecified + } + for scene in UIApplication.shared.connectedScenes { + guard let windowScene = scene as? UIWindowScene else { continue } + for window in windowScene.windows { + window.overrideUserInterfaceStyle = style + } + } + } .overlay(alignment: .top) { Color.outerBackground .opacity(0.9) diff --git a/ListlessiOS/Views/AboutView.swift b/ListlessiOS/Views/AboutView.swift @@ -0,0 +1,66 @@ +import SwiftUI + +struct AboutView: View { + private var appVersion: String { + Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown" + } + + private var buildNumber: String { + Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown" + } + + var body: some View { + List { + Section { + VStack(spacing: 12) { + Image("AboutIcon") + .resizable() + .scaledToFit() + .frame(width: 100, height: 100) + .clipShape(RoundedRectangle(cornerRadius: 22)) + + Text("Listless") + .font(.title2) + .fontWeight(.bold) + + Text("Version \(appVersion) (\(buildNumber))") + .font(.subheadline) + .foregroundStyle(.secondary) + .padding(.bottom, 8) + + Text("Made in Tokyo by Michael Camilleri") + .font(.headline) + .foregroundStyle(.primary) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 12) + .padding(.bottom, 4) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + } + + Section { + Link(destination: URL(string: "https://apps.inqk.net/listless")!) { + Label("Website", systemImage: "globe") + } + Link(destination: URL(string: "https://github.com/pyrmont/listless")!) { + Label("Source Code", systemImage: "chevron.left.forwardslash.chevron.right") + } + } + + Section { + VStack(alignment: .leading, spacing: 12) { + Text( + "Thank you to my wife, daughter and sons for their love and inspiration." + ) + } + .font(.subheadline) + .foregroundStyle(.secondary) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + } + } + .navigationTitle("About") + .navigationBarTitleDisplayMode(.inline) + } +} diff --git a/ListlessiOS/Views/SettingsView.swift b/ListlessiOS/Views/SettingsView.swift @@ -0,0 +1,46 @@ +import SwiftUI + +struct SettingsView: View { + @ObservedObject var syncMonitor: CloudKitSyncMonitor + @Environment(\.dismiss) private var dismiss + @AppStorage("headingText") private var headingText = "Items" + @AppStorage("appearanceMode") private var appearanceMode = 0 + + var body: some View { + NavigationStack { + List { + Section("Heading") { + TextField("Heading", text: $headingText) + } + + Section("Appearance") { + Picker("Appearance", selection: $appearanceMode) { + Text("System").tag(0) + Text("Light").tag(1) + Text("Dark").tag(2) + } + .pickerStyle(.segmented) + } + + Section { + NavigationLink("iCloud Diagnostics") { + SyncDiagnosticsView(syncMonitor: syncMonitor) + } + } + + Section { + NavigationLink("About") { + AboutView() + } + } + } + .navigationTitle("Settings") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Done") { dismiss() } + } + } + } + } +} diff --git a/ListlessiOS/Views/SyncDiagnosticsView.swift b/ListlessiOS/Views/SyncDiagnosticsView.swift @@ -0,0 +1,94 @@ +import SwiftUI + +struct SyncDiagnosticsView: View { + @ObservedObject var syncMonitor: CloudKitSyncMonitor + + private static let timestampFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .none + formatter.timeStyle = .medium + return formatter + }() + + var body: some View { + List { + Section("Status") { + row("Transient Banner", syncMonitor.transientErrorMessage ?? "None") + row("Last Error Domain", syncMonitor.lastCloudKitErrorDomain ?? "None") + row( + "Last Error Code", + syncMonitor.lastCloudKitErrorCode.map(String.init) ?? "None" + ) + row("Last Error Description", syncMonitor.lastCloudKitErrorDescription ?? "None") + row( + "Last Success", + syncMonitor.lastSuccessfulSyncDate.map(Self.timestampFormatter.string(from:)) + ?? "None" + ) + } + + Section("Recent Events") { + if syncMonitor.recentDiagnostics.isEmpty { + Text("No events captured yet.") + .foregroundStyle(.secondary) + } else { + ForEach(syncMonitor.recentDiagnostics.reversed()) { entry in + VStack(alignment: .leading, spacing: 4) { + Text( + "\(Self.timestampFormatter.string(from: entry.timestamp)) [\(entry.level.uppercased())]" + ) + .font(.caption.monospaced()) + .foregroundStyle(.secondary) + + Text(entry.message) + .font(.caption.monospaced()) + .textSelection(.enabled) + } + .padding(.vertical, 2) + } + } + } + } + .navigationTitle("Sync Diagnostics") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("Copy") { + UIPasteboard.general.string = diagnosticDump + } + } + } + } + + @ViewBuilder + private func row(_ title: String, _ value: String) -> some View { + VStack(alignment: .leading, spacing: 4) { + Text(title) + .font(.caption) + .foregroundStyle(.secondary) + Text(value) + .font(.body.monospaced()) + .textSelection(.enabled) + } + .padding(.vertical, 2) + } + + private var diagnosticDump: String { + var lines: [String] = [] + lines.append("Transient Banner: \(syncMonitor.transientErrorMessage ?? "None")") + lines.append("Last Error Domain: \(syncMonitor.lastCloudKitErrorDomain ?? "None")") + lines.append("Last Error Code: \(syncMonitor.lastCloudKitErrorCode.map(String.init) ?? "None")") + lines.append("Last Error Description: \(syncMonitor.lastCloudKitErrorDescription ?? "None")") + lines.append( + "Last Success: \(syncMonitor.lastSuccessfulSyncDate.map(Self.timestampFormatter.string(from:)) ?? "None")" + ) + lines.append("") + lines.append("Recent Events:") + for entry in syncMonitor.recentDiagnostics { + lines.append( + "\(Self.timestampFormatter.string(from: entry.timestamp)) [\(entry.level.uppercased())] \(entry.message)" + ) + } + return lines.joined(separator: "\n") + } +} diff --git a/ListlessiOS/Views/TaskListView.swift b/ListlessiOS/Views/TaskListView.swift @@ -14,6 +14,7 @@ struct TaskListView: View, TaskListViewProtocol { var pullUpOffset: CGFloat = 0 var isDragging: Bool = false var isShowingSyncDiagnostics = false + var isShowingSettings = false var clearingTaskIDs: Set<UUID> = [] var rowFrames: [UUID: CGRect] = [:] } @@ -22,6 +23,7 @@ struct TaskListView: View, TaskListViewProtocol { var refreshID = UUID() } + @AppStorage("headingText") var headingText = "Items" @Environment(\.undoManager) var undoManager @Environment(\.managedObjectContext) var managedObjectContext @@ -152,6 +154,10 @@ struct TaskListView: View, TaskListViewProtocol { iState.isShowingSyncDiagnostics = true } + func showSettings() { + iState.isShowingSettings = true + } + var body: some View { taskScrollView .contentShape(Rectangle()) @@ -202,7 +208,22 @@ struct TaskListView: View, TaskListViewProtocol { } .focusedSceneValue(\.taskActions, currentTaskActions) .sheet(isPresented: isShowingSyncDiagnosticsStateBinding) { - SyncDiagnosticsView(syncMonitor: syncMonitor) + NavigationStack { + SyncDiagnosticsView(syncMonitor: syncMonitor) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Close") { iState.isShowingSyncDiagnostics = false } + } + } + } + } + .sheet( + isPresented: Binding( + get: { iState.isShowingSettings }, + set: { iState.isShowingSettings = $0 } + ) + ) { + SettingsView(syncMonitor: syncMonitor) } .alert( item: Binding( @@ -304,7 +325,10 @@ struct TaskListView: View, TaskListViewProtocol { fState.focusedField = newValue handleFocusChange(from: oldValue, to: newValue) - if newValue == nil { + if newValue == nil, + !iState.isShowingSettings, + !iState.isShowingSyncDiagnostics + { if let pending = pendingFocus { focusedFieldBinding = pending fState.focusedField = pending @@ -370,102 +394,3 @@ struct TaskListView: View, TaskListViewProtocol { ) } } - -private struct SyncDiagnosticsView: View { - @ObservedObject var syncMonitor: CloudKitSyncMonitor - @Environment(\.dismiss) private var dismiss - - private static let timestampFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .none - formatter.timeStyle = .medium - return formatter - }() - - var body: some View { - NavigationStack { - List { - Section("Status") { - row("Transient Banner", syncMonitor.transientErrorMessage ?? "None") - row("Last Error Domain", syncMonitor.lastCloudKitErrorDomain ?? "None") - row( - "Last Error Code", - syncMonitor.lastCloudKitErrorCode.map(String.init) ?? "None" - ) - row("Last Error Description", syncMonitor.lastCloudKitErrorDescription ?? "None") - row( - "Last Success", - syncMonitor.lastSuccessfulSyncDate.map(Self.timestampFormatter.string(from:)) - ?? "None" - ) - } - - Section("Recent Events") { - if syncMonitor.recentDiagnostics.isEmpty { - Text("No events captured yet.") - .foregroundStyle(.secondary) - } else { - ForEach(syncMonitor.recentDiagnostics.reversed()) { entry in - VStack(alignment: .leading, spacing: 4) { - Text( - "\(Self.timestampFormatter.string(from: entry.timestamp)) [\(entry.level.uppercased())]" - ) - .font(.caption.monospaced()) - .foregroundStyle(.secondary) - - Text(entry.message) - .font(.caption.monospaced()) - .textSelection(.enabled) - } - .padding(.vertical, 2) - } - } - } - } - .navigationTitle("Sync Diagnostics") - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .topBarLeading) { - Button("Close") { dismiss() } - } - ToolbarItem(placement: .topBarTrailing) { - Button("Copy") { - UIPasteboard.general.string = diagnosticDump - } - } - } - } - } - - @ViewBuilder - private func row(_ title: String, _ value: String) -> some View { - VStack(alignment: .leading, spacing: 4) { - Text(title) - .font(.caption) - .foregroundStyle(.secondary) - Text(value) - .font(.body.monospaced()) - .textSelection(.enabled) - } - .padding(.vertical, 2) - } - - private var diagnosticDump: String { - var lines: [String] = [] - lines.append("Transient Banner: \(syncMonitor.transientErrorMessage ?? "None")") - lines.append("Last Error Domain: \(syncMonitor.lastCloudKitErrorDomain ?? "None")") - lines.append("Last Error Code: \(syncMonitor.lastCloudKitErrorCode.map(String.init) ?? "None")") - lines.append("Last Error Description: \(syncMonitor.lastCloudKitErrorDescription ?? "None")") - lines.append( - "Last Success: \(syncMonitor.lastSuccessfulSyncDate.map(Self.timestampFormatter.string(from:)) ?? "None")" - ) - lines.append("") - lines.append("Recent Events:") - for entry in syncMonitor.recentDiagnostics { - lines.append( - "\(Self.timestampFormatter.string(from: entry.timestamp)) [\(entry.level.uppercased())] \(entry.message)" - ) - } - return lines.joined(separator: "\n") - } -} diff --git a/Media.xcassets/AboutIcon.imageset/Contents.json b/Media.xcassets/AboutIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Icon.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Media.xcassets/AboutIcon.imageset/Icon.png b/Media.xcassets/AboutIcon.imageset/Icon.png Binary files differ.