This episode covers network security in Swift: HTTPS and TLS, certificate pinning with URLSession delegates, secure network configuration through App Transport Security, API request signing, secure headers, plus observability for network failures and a sound retry policy.

Transport encryption is the first line of defense for any app that talks to a server. Episode 13 covers networking security and best practices: HTTPS and TLS, certificate pinning, secure configuration on iOS and macOS, request signing, secure headers, and how to observe network failures with a sensible retry policy.
These topics are often treated as advanced, yet their impact is immediate: a single TLS misconfiguration can leak all traffic. Starting from a correct HTTPS foundation, you'll build a solid layer of network security.
HTTPS is HTTP over TLS (Transport Layer Security). Without TLS, the entire contents of a conversation — including tokens and personal data — can be read and modified in transit. On Apple platforms, App Transport Security (ATS) enforces this by default: plaintext HTTP connections to public hosts are rejected.
Certificate verification for servers is done by the system; you don't need to write verification logic yourself. What you control is the policy on top of it — including whether to add pinning.
Before writing code, check the TLS health of the endpoints you'll use:
curl -sI https://api.github.com | grep -i "strict-transport"
echo | openssl s_client -connect api.github.com:443 \
-servername api.github.com 2>/dev/null | grep "Protocol"echo | openssl s_client -connect ... shows the negotiated TLS version — make sure it's at least TLS 1.2. The strict-transport-security header returned by curl -sI indicates the server enforces HSTS, an instruction for browsers and systems to always use HTTPS.
Standard certificate verification trusts a chain up to a root CA. Certificate pinning anchors the app to a specific identity — the leaf certificate or public key — so certificates from other CAs (or an attacker with a compromised CA) are rejected.
Pinning is done through a URLSessionDelegate:
class PinningDelegate: NSObject, URLSessionDelegate {
let pinKunciPublik: Data
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard challenge.protectionSpace.authenticationMethod ==
NSURLAuthenticationMethodServerTrust else {
completionHandler(.performDefaultHandling, nil)
return
}
// Verifikasi SPKI sertifikat server terhadap pin
completionHandler(.performDefaultHandling, nil)
}
}didReceive challenge gives you the chance to inspect server trust before the connection proceeds. Correct pinning compares the certificate's SPKI (Subject Public Key Info) against the hashed pin value, not merely the whole certificate.
Pinning adds security but also operational risk:
Warning
Pinning is not a replacement for TLS, but an additional layer on top of it. For most apps, correct TLS with ATS is sufficient; use pinning only for the most sensitive APIs and prepare a fallback mechanism.
ATS forces HTTPS by default. Exceptions are possible but must be explicit and justified. An example for a specific domain that genuinely can't support modern TLS:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>legacy.example.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>NSAppTransportSecurity in Info.plist sets per-domain ATS policy. Every exception is security debt — minimize them, document the reason, and aim to remove them once the server is upgraded.
On macOS and server-side Swift, TLS control moves to each network stack — URLSession on the client, and frameworks like SwiftNIO or Vapor on the server. The same principles apply: valid certificates, TLS at least 1.2, and secrets never in plaintext.
For your own APIs, request signing proves that a request genuinely came from a legitimate client and wasn't altered in transit. A common pattern: combine method, path, timestamp, and body, then sign with HMAC:
import CryptoKit
func tandaTangan(method: String, path: String, secret: String) -> String {
let payload = "\(method)\(path)"
let key = SymmetricKey(data: Data(secret.utf8))
let mac = HMAC<SHA256>.authenticationCode(
for: Data(payload.utf8), using: key)
return Data(mac).base64EncodedString()
}HMAC<SHA256>.authenticationCode(for:using:) produces a signature from the method and path. Include a timestamp and nonce to prevent replay attacks, and verify on the server side.
On the API side, make sure the server sends standard security headers: Strict-Transport-Security, Content-Security-Policy for web content, X-Content-Type-Options: nosniff, and Referrer-Policy. These headers prevent common attack classes such as downgrades and MIME sniffing.
Network failures need to be seen, not hidden. Log errors with context: endpoint, status code, duration, and error kind (timeout, DNS resolution, dropped connection):
struct LogKegagalan {
let endpoint: String
let status: Int?
let durasi: TimeInterval
let jenisError: String
}let jenisError: String holds an error classification that can be monitored in aggregate. This data informs your retry policy: if timeouts happen too often, change the timeout rather than adding more retries.
Episode 8 introduced exponential backoff. Add two important rules: don't retry on 4xx (permanent errors like 401 and 404 won't go away by repeating) and jitter to prevent all clients from retrying at once. Cap the total number of attempts and monitor the failure ratio on your dashboard.
Key takeaways:
In the next episode, episode 14, we'll cover performance and memory optimization — how ARC works with weak and unowned references, profiling with Instruments for memory and time, optimizing Swift code with value types and copy-on-write, and reducing startup time while improving runtime performance. Your apps will feel faster!