summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-03 16:17:43 -0500
committerChristian Cleberg <[email protected]>2026-04-03 16:17:43 -0500
commitede15c586af71b8d6f2c190ffdad0324414cac06 (patch)
tree4d3676531b62d7bde7d18bb5030be464dc8db0ac
parent376e7b55b4a0c4f54faa0c2828dea4fd17dff288 (diff)
downloaddomain-dig-ede15c586af71b8d6f2c190ffdad0324414cac06.tar.gz
domain-dig-ede15c586af71b8d6f2c190ffdad0324414cac06.tar.bz2
domain-dig-ede15c586af71b8d6f2c190ffdad0324414cac06.zip
fix: various bugs
- fix Email section to left align - ensure container contents can overflow scroll but page cannot - fix map width
-rw-r--r--DomainDig.xcodeproj/project.pbxproj8
-rw-r--r--DomainDig/ContentView.swift77
-rw-r--r--DomainDig/HistoryView.swift77
-rw-r--r--claude.md171
4 files changed, 94 insertions, 239 deletions
diff --git a/DomainDig.xcodeproj/project.pbxproj b/DomainDig.xcodeproj/project.pbxproj
index f37b290..12f8baf 100644
--- a/DomainDig.xcodeproj/project.pbxproj
+++ b/DomainDig.xcodeproj/project.pbxproj
@@ -265,7 +265,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 3;
+ CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = ZCNAX3VL9D;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -282,7 +282,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.2;
+ MARKETING_VERSION = 1.2.1;
PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -301,7 +301,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
- CURRENT_PROJECT_VERSION = 3;
+ CURRENT_PROJECT_VERSION = 4;
DEVELOPMENT_TEAM = ZCNAX3VL9D;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -318,7 +318,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 1.2;
+ MARKETING_VERSION = 1.2.1;
PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.DomainDig;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
diff --git a/DomainDig/ContentView.swift b/DomainDig/ContentView.swift
index 940a25b..b6a7607 100644
--- a/DomainDig/ContentView.swift
+++ b/DomainDig/ContentView.swift
@@ -7,7 +7,7 @@ struct ContentView: View {
var body: some View {
NavigationStack {
- ScrollView {
+ ScrollView(.vertical) {
VStack(spacing: 0) {
inputSection
if viewModel.hasRun {
@@ -225,7 +225,7 @@ struct ContentView: View {
.background(Color(.systemGray6).opacity(0.5))
.cornerRadius(6)
} else {
- VStack(alignment: .leading, spacing: 4) {
+ horizontallyScrollableCard {
ForEach(viewModel.redirectChain) { hop in
HStack(alignment: .top, spacing: 6) {
Text("\(hop.stepNumber)")
@@ -248,9 +248,6 @@ struct ContentView: View {
}
}
}
- .padding(10)
- .background(Color(.systemGray6).opacity(0.5))
- .cornerRadius(6)
}
}
.padding(.top, 16)
@@ -281,7 +278,7 @@ struct ContentView: View {
}
private var ptrRow: some View {
- VStack(alignment: .leading, spacing: 4) {
+ horizontallyScrollableCard {
Text("PTR (Reverse DNS)")
.font(.system(.subheadline, design: .monospaced))
.fontWeight(.semibold)
@@ -302,13 +299,10 @@ struct ContentView: View {
.foregroundStyle(.secondary)
}
}
- .padding(10)
- .background(Color(.systemGray6).opacity(0.5))
- .cornerRadius(6)
}
private func dnsRecordSection(_ section: DNSSection) -> some View {
- VStack(alignment: .leading, spacing: 4) {
+ horizontallyScrollableCard {
Text(section.recordType.rawValue)
.font(.system(.subheadline, design: .monospaced))
.fontWeight(.semibold)
@@ -334,9 +328,6 @@ struct ContentView: View {
dnsRecordRows(section.wildcardRecords)
}
}
- .padding(10)
- .background(Color(.systemGray6).opacity(0.5))
- .cornerRadius(6)
}
private func dnsRecordRows(_ records: [DNSRecord]) -> some View {
@@ -369,16 +360,14 @@ struct ContentView: View {
} else if let error = viewModel.emailSecurityError {
errorLabel(error)
} else if let email = viewModel.emailSecurity {
- VStack(alignment: .leading, spacing: 6) {
+ horizontallyScrollableCard(spacing: 6) {
emailSecurityRow("SPF", record: email.spf)
emailSecurityRow("DMARC", record: email.dmarc)
emailSecurityRow("DKIM", record: email.dkim)
}
- .padding(10)
- .background(Color(.systemGray6).opacity(0.5))
- .cornerRadius(6)
}
}
+ .frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, 16)
}
@@ -434,7 +423,7 @@ struct ContentView: View {
}
private func sslDetail(_ info: SSLCertificateInfo) -> some View {
- VStack(alignment: .leading, spacing: 8) {
+ horizontallyScrollableCard(spacing: 8) {
certRow("Common Name", info.commonName)
certRow("Issuer", info.issuer)
@@ -466,9 +455,6 @@ struct ContentView: View {
certRow("Chain Depth", "\(info.chainDepth)")
}
- .padding(10)
- .background(Color(.systemGray6).opacity(0.5))
- .cornerRadius(6)
}
// MARK: - HTTP Headers
@@ -484,7 +470,7 @@ struct ContentView: View {
} else if let error = viewModel.httpHeadersError {
errorLabel(error)
} else {
- VStack(alignment: .leading, spacing: 4) {
+ horizontallyScrollableCard {
ForEach(viewModel.httpHeaders) { header in
HStack(alignment: .top, spacing: 4) {
Text(header.name + ":")
@@ -497,9 +483,6 @@ struct ContentView: View {
}
}
}
- .padding(10)
- .background(Color(.systemGray6).opacity(0.5))
- .cornerRadius(6)
}
}
.padding(.top, 16)
@@ -533,14 +516,17 @@ struct ContentView: View {
private func ipGeolocationDetail(_ geo: IPGeolocation) -> some View {
VStack(alignment: .leading, spacing: 6) {
- certRow("IP", geo.ip)
- if let org = geo.org {
- certRow("Org / ISP", org)
- }
- let location = [geo.city, geo.region, geo.country_name].compactMap { $0 }.joined(separator: ", ")
- if !location.isEmpty {
- certRow("Location", location)
+ horizontallyScrollableContent(spacing: 6) {
+ certRow("IP", geo.ip)
+ if let org = geo.org {
+ certRow("Org / ISP", org)
+ }
+ let location = [geo.city, geo.region, geo.country_name].compactMap { $0 }.joined(separator: ", ")
+ if !location.isEmpty {
+ certRow("Location", location)
+ }
}
+
if let lat = geo.latitude, let lon = geo.longitude {
let coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lon)
Map(initialPosition: .region(MKCoordinateRegion(
@@ -550,6 +536,7 @@ struct ContentView: View {
Marker(geo.ip, coordinate: coordinate)
}
.mapStyle(.standard)
+ .frame(maxWidth: .infinity)
.frame(height: 180)
.cornerRadius(8)
}
@@ -620,6 +607,32 @@ struct ContentView: View {
}
}
+ private func horizontallyScrollableCard<Content: View>(
+ spacing: CGFloat = 4,
+ @ViewBuilder content: () -> Content
+ ) -> some View {
+ horizontallyScrollableContent(spacing: spacing) {
+ content()
+ }
+ .padding(10)
+ .background(Color(.systemGray6).opacity(0.5))
+ .cornerRadius(6)
+ }
+
+ private func horizontallyScrollableContent<Content: View>(
+ spacing: CGFloat = 4,
+ @ViewBuilder content: () -> Content
+ ) -> some View {
+ ScrollView(.horizontal) {
+ VStack(alignment: .leading, spacing: spacing) {
+ content()
+ }
+ .scrollTargetLayout()
+ }
+ .scrollBounceBehavior(.basedOnSize, axes: .horizontal)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
private func errorLabel(_ message: String) -> some View {
Label(message, systemImage: "exclamationmark.triangle.fill")
.font(.system(.caption, design: .monospaced))
diff --git a/DomainDig/HistoryView.swift b/DomainDig/HistoryView.swift
index b64ebf3..1963509 100644
--- a/DomainDig/HistoryView.swift
+++ b/DomainDig/HistoryView.swift
@@ -65,7 +65,7 @@ struct HistoryDetailView: View {
}()
var body: some View {
- ScrollView {
+ ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 0) {
cachedBanner
reachabilitySection
@@ -157,7 +157,7 @@ struct HistoryDetailView: View {
.background(Color(.systemGray6).opacity(0.5))
.cornerRadius(6)
} else {
- VStack(alignment: .leading, spacing: 4) {
+ horizontallyScrollableCard {
ForEach(entry.redirectChain) { hop in
HStack(alignment: .top, spacing: 6) {
Text("\(hop.stepNumber)")
@@ -180,9 +180,6 @@ struct HistoryDetailView: View {
}
}
}
- .padding(10)
- .background(Color(.systemGray6).opacity(0.5))
- .cornerRadius(6)
}
}
}
@@ -195,7 +192,7 @@ struct HistoryDetailView: View {
VStack(alignment: .leading, spacing: 12) {
sectionHeader("DNS Records")
ForEach(entry.dnsSections) { section in
- VStack(alignment: .leading, spacing: 4) {
+ horizontallyScrollableCard {
Text(section.recordType.rawValue)
.font(.system(.subheadline, design: .monospaced))
.fontWeight(.semibold)
@@ -220,12 +217,9 @@ struct HistoryDetailView: View {
recordRows(section.wildcardRecords)
}
}
- .padding(10)
- .background(Color(.systemGray6).opacity(0.5))
- .cornerRadius(6)
if section.recordType == .A {
- VStack(alignment: .leading, spacing: 4) {
+ horizontallyScrollableCard {
Text("PTR (Reverse DNS)")
.font(.system(.subheadline, design: .monospaced))
.fontWeight(.semibold)
@@ -242,9 +236,6 @@ struct HistoryDetailView: View {
.foregroundStyle(.secondary)
}
}
- .padding(10)
- .background(Color(.systemGray6).opacity(0.5))
- .cornerRadius(6)
}
}
}
@@ -259,16 +250,14 @@ struct HistoryDetailView: View {
VStack(alignment: .leading, spacing: 12) {
if let email = entry.emailSecurity {
sectionHeader("Email Security")
- VStack(alignment: .leading, spacing: 6) {
+ horizontallyScrollableCard(spacing: 6) {
historyEmailRow("SPF", record: email.spf)
historyEmailRow("DMARC", record: email.dmarc)
historyEmailRow("DKIM", record: email.dkim)
}
- .padding(10)
- .background(Color(.systemGray6).opacity(0.5))
- .cornerRadius(6)
}
}
+ .frame(maxWidth: .infinity, alignment: .leading)
.padding(.top, 16)
}
@@ -310,7 +299,7 @@ struct HistoryDetailView: View {
VStack(alignment: .leading, spacing: 12) {
if let info = entry.sslInfo {
sectionHeader("SSL / TLS Certificate")
- VStack(alignment: .leading, spacing: 8) {
+ horizontallyScrollableCard(spacing: 8) {
labelRow("Common Name", info.commonName)
labelRow("Issuer", info.issuer)
@@ -341,9 +330,6 @@ struct HistoryDetailView: View {
labelRow("Chain Depth", "\(info.chainDepth)")
}
- .padding(10)
- .background(Color(.systemGray6).opacity(0.5))
- .cornerRadius(6)
}
}
.padding(.top, 16)
@@ -355,7 +341,7 @@ struct HistoryDetailView: View {
VStack(alignment: .leading, spacing: 12) {
if !entry.httpHeaders.isEmpty {
sectionHeader("HTTP Headers")
- VStack(alignment: .leading, spacing: 4) {
+ horizontallyScrollableCard {
ForEach(entry.httpHeaders) { header in
HStack(alignment: .top, spacing: 4) {
Text(header.name + ":")
@@ -368,9 +354,6 @@ struct HistoryDetailView: View {
}
}
}
- .padding(10)
- .background(Color(.systemGray6).opacity(0.5))
- .cornerRadius(6)
}
}
.padding(.top, 16)
@@ -383,14 +366,17 @@ struct HistoryDetailView: View {
if let geo = entry.ipGeolocation {
sectionHeader("IP Location")
VStack(alignment: .leading, spacing: 6) {
- labelRow("IP", geo.ip)
- if let org = geo.org {
- labelRow("Org / ISP", org)
- }
- let location = [geo.city, geo.region, geo.country_name].compactMap { $0 }.joined(separator: ", ")
- if !location.isEmpty {
- labelRow("Location", location)
+ horizontallyScrollableContent(spacing: 6) {
+ labelRow("IP", geo.ip)
+ if let org = geo.org {
+ labelRow("Org / ISP", org)
+ }
+ let location = [geo.city, geo.region, geo.country_name].compactMap { $0 }.joined(separator: ", ")
+ if !location.isEmpty {
+ labelRow("Location", location)
+ }
}
+
if let lat = geo.latitude, let lon = geo.longitude {
let coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lon)
Map(initialPosition: .region(MKCoordinateRegion(
@@ -400,6 +386,7 @@ struct HistoryDetailView: View {
Marker(geo.ip, coordinate: coordinate)
}
.mapStyle(.standard)
+ .frame(maxWidth: .infinity)
.frame(height: 180)
.cornerRadius(8)
}
@@ -481,6 +468,32 @@ struct HistoryDetailView: View {
}
}
+ private func horizontallyScrollableCard<Content: View>(
+ spacing: CGFloat = 4,
+ @ViewBuilder content: () -> Content
+ ) -> some View {
+ horizontallyScrollableContent(spacing: spacing) {
+ content()
+ }
+ .padding(10)
+ .background(Color(.systemGray6).opacity(0.5))
+ .cornerRadius(6)
+ }
+
+ private func horizontallyScrollableContent<Content: View>(
+ spacing: CGFloat = 4,
+ @ViewBuilder content: () -> Content
+ ) -> some View {
+ ScrollView(.horizontal) {
+ VStack(alignment: .leading, spacing: spacing) {
+ content()
+ }
+ .scrollTargetLayout()
+ }
+ .scrollBounceBehavior(.basedOnSize, axes: .horizontal)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+
private func errorLabel(_ message: String) -> some View {
Label(message, systemImage: "exclamationmark.triangle.fill")
.font(.system(.caption, design: .monospaced))
diff --git a/claude.md b/claude.md
deleted file mode 100644
index bee59f1..0000000
--- a/claude.md
+++ /dev/null
@@ -1,171 +0,0 @@
-# DomainDig – Project Context
-
-## What this is
-An iOS utility app for querying DNS records, inspecting SSL/TLS certificates,
-checking HTTP headers, measuring TCP reachability, geolocating IP addresses,
-analyzing email security records, tracing redirect chains, performing reverse
-DNS lookups, and scanning common ports for any domain. Designed for developers,
-sysadmins, and anyone who manages or troubleshoots domains and servers.
-
-## Target user
-Technically literate adults — developers, IT/sysadmin, homelab enthusiasts.
-Comfortable with terms like A record, CNAME, MX, TTL, and TLS. The UI should be
-clean and information-dense, not hand-holding.
-
-## Design principles
-- Minimal, utilitarian UI — this is a tool, not a consumer app
-- Results should be readable at a glance — use clear labels, monospace where
- appropriate for IPs and raw values
-- Dark mode preferred as the primary aesthetic
-- No onboarding, no tutorials, no splash screens
-- Fast — results should appear as soon as they're available, not after all
- lookups complete
-- Each result section loads independently with its own ProgressView
-- Errors shown inline per-section, never as global alerts
-
-## Features
-
-### DNS Lookup
-Query the following record types for any domain via DNS-over-HTTPS (use
-Cloudflare 1.1.1.1 — https://cloudflare-dns.com/dns-query):
-- A
-- AAAA
-- MX
-- NS
-- TXT
-- CNAME
-
-Display each record type in its own section. Show TTL alongside each result.
-If a record type returns no results, show "No records found" for that type
-rather than hiding the section entirely.
-
-After the apex query, also queries `*.{domain}` for A, AAAA, MX, and TXT.
-Wildcard results are shown as a sub-section beneath each record type, labelled
-with `*.{domain}`. Hidden if no wildcard records are returned.
-
-### SSL/TLS Certificate Check
-Connect to the domain on port 443 via URLSession and inspect the server's
-certificate chain using URLSession delegate methods. Display:
-- Common name (CN)
-- Subject Alternative Names (SANs)
-- Issuer
-- Valid from / Valid until dates
-- Days until expiry — highlight in red if under 30 days, yellow if under 60
-- Certificate chain depth
-
-### HTTP Headers Check
-Fire a HEAD request to `https://{domain}` and display all response headers.
-Header names in cyan, values in primary. Security-relevant headers highlighted
-in yellow: `strict-transport-security`, `x-frame-options`,
-`x-content-type-options`, `content-security-policy`, `referrer-policy`.
-Service: `HTTPHeadersService.swift`.
-
-### Reachability / TCP Latency
-Use `NWConnection` (Network framework) to attempt TCP connections on ports 443
-and 80. Measure time to `.ready` state. Display green/red dot, latency in ms,
-and reachable/unreachable status. Timeout after 5 seconds.
-Service: `ReachabilityService.swift`.
-
-### IP Geolocation with Map
-After DNS A record resolves, take the first IP and query
-`https://ipapi.co/{ip}/json/` for country, region, city, org, lat/lon.
-Display in an "IP Location" section with a SwiftUI `Map` (MapKit) centered on
-the coordinates with a `Marker`. Map height 180pt, `.standard` style.
-Service: `IPGeolocationService.swift`.
-
-### Email Security Analysis
-Parse SPF from existing TXT records (no extra query). Query `_dmarc.{domain}`
-for DMARC and try common DKIM selectors (`default`, `google`, `mail`) via DoH.
-Display green checkmark if found, red ✗ if not. Full record values truncated
-to 80 chars with tap-to-expand. Triggered after DNS completes.
-Service: `EmailSecurityService.swift`.
-
-### Reverse DNS / PTR Lookup
-After DNS A record resolves, construct reverse DNS name (reversed octets +
-`.in-addr.arpa`) and query PTR record via Cloudflare DoH. Displayed inline
-in the DNS Records section below the A record sub-section.
-Service: `ReverseDNSService.swift`.
-
-### Redirect Chain
-Fire HTTP request to `http://{domain}` with redirects disabled. Follow up to
-10 redirects manually, recording each hop's URL and status code. Display step
-number, status code in cyan, URL, and "(final)" on the last hop. Shows
-"No redirects — direct connection" if the first request returns 200.
-Service: `RedirectChainService.swift`.
-
-### Common Port Scanner
-Probe 10 common ports (21/FTP, 22/SSH, 25/SMTP, 80/HTTP, 443/HTTPS,
-587/SMTP-TLS, 3306/MySQL, 5432/PostgreSQL, 8080/HTTP-Alt, 8443/HTTPS-Alt)
-using `NWConnection` with 3-second timeout. All probes run concurrently.
-Green dot for open, grey dot for closed. Closed is expected — no error shown.
-Service: `PortScanService.swift`.
-
-### Results layout
-- Domain input at the top — large text field, keyboard shows on launch
-- Run button to trigger all lookups simultaneously
-- Results displayed in clearly separated sections:
- Reachability → Redirect Chain → DNS Records (with PTR inline) →
- Email Security → SSL/TLS Certificate → HTTP Headers → IP Location →
- Open Ports
-- Each section loads independently — don't block one section waiting for another
-- Email security, PTR, and IP geolocation are chained after DNS; all others
- run in parallel
-
-### Recent searches
-Store the last 20 searched domains locally using UserDefaults. Display them as
-a tappable list below the text field when no results are showing. Tapping a
-recent domain populates the text field and runs the lookup immediately. Include
-a "Clear" button to wipe history. Most recent at the top.
-
-### Saved domains
-Bookmark button (SF Symbol: `bookmark` / `bookmark.fill`) in the results
-toolbar area, next to the share button. Tapping saves/unsaves the current
-domain. Filled icon when saved. Saved domains viewable from a toolbar button
-that pushes to `SavedDomainsView` — a list of saved domains, tappable to run
-lookups, with swipe-to-delete and an Edit button for bulk deletion.
-Persisted in UserDefaults under key `savedDomains`.
-
-### Lookup history with cached results
-After each successful lookup, a snapshot of all results (DNS, SSL, HTTP headers,
-reachability, geolocation, email security, PTR, redirect chain, port scan) is
-saved to history in UserDefaults as JSON. Capped at 50 entries. `HistoryView`
-shows past lookups with domain and timestamp. Tapping shows full cached results
-in `HistoryDetailView` using the same layout, labelled as cached with the
-original timestamp. Model: `HistoryEntry` (Codable).
-
-### Share / export
-A share button (SF Symbol: `square.and.arrow.up`) in the top-right of the
-results area. Formats the full results as plain text and presents the iOS share
-sheet via UIActivityViewController. The export includes: domain, timestamp,
-reachability, redirect chain, all DNS records with TTLs and PTR, email security
-records, SSL cert fields, HTTP headers, IP geolocation data, and port scan
-results.
-
-## Technical constraints
-- SwiftUI, iOS only
-- Fully offline except for network requests (DNS-over-HTTPS, SSL, HTTP HEAD,
- TCP connections, geolocation API)
-- No accounts, no analytics, no ads
-- No third-party dependencies — URLSession, Network, MapKit only
-- Targets latest iOS
-
-## Architecture
-- `Models.swift` — All data models (DNS, SSL, HTTP headers, reachability,
- geolocation, history entry), all Codable for persistence
-- `DomainViewModel.swift` — `@Observable` view model orchestrating all lookups,
- managing state, history, saved domains, recent searches, and export
-- `ContentView.swift` — Main screen with input, all result sections, toolbar
-- `SavedDomainsView.swift` — Saved domains list with edit/delete
-- `HistoryView.swift` — History list + `HistoryDetailView` for cached results
-- Services: `DNSLookupService`, `SSLCheckService`, `HTTPHeadersService`,
- `ReachabilityService`, `IPGeolocationService`, `EmailSecurityService`,
- `ReverseDNSService`, `RedirectChainService`, `PortScanService`
-
-## What good looks like
-A developer pastes a domain, taps run, and within a couple of seconds sees a
-clean breakdown of TCP reachability, redirect chain, every DNS record type with
-reverse DNS, email security posture, the full SSL cert status, HTTP response
-headers with security headers highlighted, IP geolocation with a map, and a
-port scan of common services. It should feel like a native, polished version of
-running `dig`, `openssl s_client`, `curl -I`, `nmap`, and `whois` from the
-terminal.