Skip to content

Commit 15a3762

Browse files
authored
Merge pull request #2 from Rev0212/feature-customise
Feature customise
2 parents da0ce50 + 628e14c commit 15a3762

8 files changed

Lines changed: 515 additions & 64 deletions

File tree

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,28 @@ Enable the inspector in **DEBUG** builds only:
3535
NetworkInspector.LeapInspector.enable()
3636
NetworkInspector.LeapInspector.enableFloatingButton()
3737
#endif
38+
```
3839

3940
A floating 📡 button will appear on screen.
4041
Tap it to open the network inspector UI.
42+
43+
To capture only specific base URLs, pass one or more base addresses:
44+
45+
```swift
46+
#if DEBUG
47+
NetworkInspector.enable(
48+
baseURLs: [
49+
"https://mario-api.leapscholar.com",
50+
"https://mario-ieltsbff.leapscholar.com"
51+
]
52+
)
53+
NetworkInspector.enableFloatingButton()
54+
#endif
55+
```
56+
57+
---
58+
59+
## API Tab
60+
61+
The inspector UI includes an **APIs** tab that lists unique endpoints and the number
62+
of calls for each. Tap an endpoint to view only the logs for that API.

Sources/NetworkInspector/InspectorURLProtocol.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ final class InspectorURLProtocol: URLProtocol {
2727
}
2828

2929
guard let scheme = request.url?.scheme else { return false }
30-
return scheme == "http" || scheme == "https"
30+
guard scheme == "http" || scheme == "https" else { return false }
31+
return NetworkInterceptor.shouldIntercept(request.url)
3132
}
3233

3334
override class func canonicalRequest(for request: URLRequest) -> URLRequest {

Sources/NetworkInspector/LeapInspector.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ public enum LeapInspector {
77

88
private static var isEnabled = false
99

10-
public static func enable() {
10+
public static func enable(baseURLs: [String] = []) {
1111
guard !isEnabled else { return }
1212
isEnabled = true
13+
NetworkInterceptor.setAllowedBaseURLs(baseURLs)
1314
NetworkInterceptor.register()
1415
}
1516

Sources/NetworkInspector/NetworkInterceptor.swift

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,39 @@ import Foundation
1010

1111
final class NetworkInterceptor {
1212

13+
private static var allowedBaseURLs: [String] = []
14+
1315
static func register() {
1416
URLProtocol.registerClass(InspectorURLProtocol.self)
1517
}
1618

1719
static func unregister() {
1820
URLProtocol.unregisterClass(InspectorURLProtocol.self)
1921
}
22+
23+
static func setAllowedBaseURLs(_ urls: [String]) {
24+
allowedBaseURLs = urls
25+
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
26+
.filter { !$0.isEmpty }
27+
.map { normalizeBaseURL($0) }
28+
}
29+
30+
static func getAllowedBaseURLs() -> [String] {
31+
allowedBaseURLs
32+
}
33+
34+
static func shouldIntercept(_ url: URL?) -> Bool {
35+
guard let absoluteString = url?.absoluteString.lowercased() else { return false }
36+
guard !allowedBaseURLs.isEmpty else { return true }
37+
38+
return allowedBaseURLs.contains { absoluteString.hasPrefix($0) }
39+
}
40+
41+
private static func normalizeBaseURL(_ url: String) -> String {
42+
var normalized = url.lowercased()
43+
while normalized.hasSuffix("/") {
44+
normalized.removeLast()
45+
}
46+
return normalized
47+
}
2048
}

Sources/NetworkInspector/NetworkLogStore.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,19 @@ final class NetworkLogStore {
6060
}
6161
}
6262
}
63+
64+
func remove(where shouldRemove: @escaping (NetworkLog) -> Bool) {
65+
queue.async {
66+
self.logs.removeAll(where: shouldRemove)
67+
68+
DispatchQueue.main.async {
69+
NotificationCenter.default.post(
70+
name: .networkLogStoreDidUpdate,
71+
object: nil
72+
)
73+
}
74+
}
75+
}
6376

6477
func clear() {
6578
queue.async {
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
//
2+
// ApiListViewController.swift
3+
// NetworkInspector
4+
//
5+
// Created by Revanth A on 01/01/26.
6+
//
7+
8+
import UIKit
9+
10+
final class ApiListViewController: UIViewController {
11+
12+
private let tableView = UITableView()
13+
private var items: [APIItem] = []
14+
15+
override func viewDidLoad() {
16+
super.viewDidLoad()
17+
18+
title = "APIs"
19+
view.backgroundColor = .systemBackground
20+
21+
setupTableView()
22+
loadItems()
23+
observeLogs()
24+
}
25+
26+
deinit {
27+
NotificationCenter.default.removeObserver(self)
28+
}
29+
}
30+
31+
private extension ApiListViewController {
32+
33+
func setupTableView() {
34+
tableView.translatesAutoresizingMaskIntoConstraints = false
35+
view.addSubview(tableView)
36+
37+
NSLayoutConstraint.activate([
38+
tableView.topAnchor.constraint(equalTo: view.topAnchor),
39+
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
40+
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
41+
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
42+
])
43+
44+
tableView.dataSource = self
45+
tableView.delegate = self
46+
}
47+
48+
func observeLogs() {
49+
NotificationCenter.default.addObserver(
50+
self,
51+
selector: #selector(onLogsUpdated),
52+
name: .networkLogStoreDidUpdate,
53+
object: nil
54+
)
55+
}
56+
57+
@objc
58+
func onLogsUpdated() {
59+
loadItems()
60+
}
61+
62+
func loadItems() {
63+
let logs = NetworkLogStore.shared.getAllLogs()
64+
items = buildItems(from: logs)
65+
tableView.reloadData()
66+
}
67+
68+
func buildItems(from logs: [NetworkLog]) -> [APIItem] {
69+
var counts: [String: APIItem] = [:]
70+
71+
for log in logs {
72+
let host = log.request.url?.host ?? "-"
73+
let path = endpointPath(from: log.request.url)
74+
let key = "\(host)|\(path)"
75+
76+
if let existing = counts[key] {
77+
counts[key] = APIItem(host: host, path: path, count: existing.count + 1)
78+
} else {
79+
counts[key] = APIItem(host: host, path: path, count: 1)
80+
}
81+
}
82+
83+
return counts.values.sorted { lhs, rhs in
84+
if lhs.host == rhs.host {
85+
return lhs.path < rhs.path
86+
}
87+
return lhs.host < rhs.host
88+
}
89+
}
90+
91+
func endpointPath(from url: URL?) -> String {
92+
let path = url?.path ?? "/"
93+
if path == "/" || path.isEmpty {
94+
return "/"
95+
}
96+
return path.hasPrefix("/") ? String(path.dropFirst()) : path
97+
}
98+
}
99+
100+
extension ApiListViewController: UITableViewDataSource {
101+
102+
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
103+
items.count
104+
}
105+
106+
func tableView(
107+
_ tableView: UITableView,
108+
cellForRowAt indexPath: IndexPath
109+
) -> UITableViewCell {
110+
let cell = tableView.dequeueReusableCell(withIdentifier: "ApiCell")
111+
?? UITableViewCell(style: .subtitle, reuseIdentifier: "ApiCell")
112+
113+
let item = items[indexPath.row]
114+
cell.textLabel?.text = item.path
115+
cell.textLabel?.font = .systemFont(ofSize: 15, weight: .medium)
116+
cell.detailTextLabel?.text = "\(item.host)\(item.count) calls"
117+
cell.detailTextLabel?.textColor = .secondaryLabel
118+
cell.selectionStyle = .none
119+
120+
return cell
121+
}
122+
}
123+
124+
extension ApiListViewController: UITableViewDelegate {
125+
126+
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
127+
let item = items[indexPath.row]
128+
let title = item.path == "/" ? item.host : item.path
129+
130+
let filter: (NetworkLog) -> Bool = { log in
131+
let host = log.request.url?.host ?? "-"
132+
let path = self.endpointPath(from: log.request.url)
133+
return host == item.host && path == item.path
134+
}
135+
136+
let detailVC = LogListViewController(filter: filter, title: title)
137+
navigationController?.pushViewController(detailVC, animated: true)
138+
}
139+
}
140+
141+
private struct APIItem {
142+
let host: String
143+
let path: String
144+
let count: Int
145+
}

0 commit comments

Comments
 (0)