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:
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.