Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion HackerTube/Domain/ApiService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class ApiService {
private let client: MediaCCCApiClient

init() {
client = .init()
client = .init(urlSession: .mediaCCCApi)
}

// MARK: Conferences
Expand Down
39 changes: 39 additions & 0 deletions HackerTube/Extensions/URLSession+Extensions.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//
// URLSession+Extensions.swift
// HackerTube
//
// Created by Mathijs Bernson on 04/11/2026.
//

import Foundation
import OSLog

extension URLSession {
/// URLSession with a cache intended for talk thumbnails, conference logos, etcetera.
static let remoteImages: URLSession = {
let directory = URL.cachesDirectory.appending(path: "RemoteImages", directoryHint: .isDirectory)
let cache = URLCache(
memoryCapacity: 20_000_000, // 20 megabytes
diskCapacity: 100_000_000, // 100 megabytes
directory: directory
)
let configuration = URLSessionConfiguration.default
configuration.urlCache = cache
return URLSession(configuration: configuration)
}()

/// URLSession for making calls to the `media.ccc.de` API
static let mediaCCCApi: URLSession = {
// The API answers with `Cache-Control: max-age=0, must-revalidate` and an ETag, so every request is
// revalidated. The larger cache makes sure the 300 KB listing pages survive until the `304 Not Modified`.
let directory = URL.cachesDirectory.appending(path: "MediaCCCApi", directoryHint: .isDirectory)
let cache = URLCache(
memoryCapacity: 10_000_000, // 10 megabytes
diskCapacity: 50_000_000, // 50 megabytes
directory: directory
)
let configuration = URLSessionConfiguration.default
configuration.urlCache = cache
return URLSession(configuration: configuration)
}()
}
1 change: 1 addition & 0 deletions HackerTube/Features/Conferences/ConferenceThumbnail.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ struct ConferenceThumbnail: View {
ProgressView()
}
}
.backport.asyncImageURLSession(.remoteImages)
}
}

Expand Down
1 change: 1 addition & 0 deletions HackerTube/Features/Talk/TalkListItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ struct TalkListItem: View {
} placeholder: {
ProgressView()
}
.backport.asyncImageURLSession(.remoteImages)
.frame(width: width, height: width * (9 / 16))
}

Expand Down
2 changes: 1 addition & 1 deletion HackerTube/Features/Talk/TalkMetadataFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ struct TalkMetadataFactory {
}

func fetchImageData(forURL url: URL) async throws -> Data? {
let (imageData, _) = try await URLSession.shared.data(from: url)
let (imageData, _) = try await URLSession.remoteImages.data(from: url)
return imageData
}
}
1 change: 1 addition & 0 deletions HackerTube/Features/Talk/TalkThumbnail.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ struct TalkThumbnail: View {
ProgressView()
}
}
.backport.asyncImageURLSession(.remoteImages)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.aspectRatio(16 / 9, contentMode: .fit)
.id(id)
Expand Down
2 changes: 1 addition & 1 deletion HackerTube/Features/Talk/TalkViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class TalkViewModel {
private let mediaAnalyzer: MediaAnalyzer

init() {
client = .init()
client = .init(urlSession: .mediaCCCApi)
mediaAnalyzer = .init()
}

Expand Down
2 changes: 1 addition & 1 deletion HackerTube/SVGImageView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ struct SVGImageView<Fallback: View>: View {

didFail = false
do {
let (data, _) = try await URLSession.shared.data(from: url)
let (data, _) = try await URLSession.remoteImages.data(from: url)
try Task.checkCancellation()
let rendered = try await rasterizeSVG(data: data, size: size, scale: displayScale)
cache[key] = rendered
Expand Down
32 changes: 32 additions & 0 deletions HackerTube/ViewModifiers/Backport.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//
// Backport.swift
// HackerTube
//
// Created by Mathijs Bernson on 04/11/2026.
//

import Foundation
import SwiftUI

// Thanks to: https://davedelong.com/blog/2021/10/09/simplifying-backwards-compatibility-in-swift/
struct Backport<Content> {
let content: Content

init(_ content: Content) {
self.content = content
}
}

extension View {
var backport: Backport<Self> { Backport(self) }
}

extension Backport where Content: View {
@ViewBuilder func asyncImageURLSession(_ urlSession: URLSession) -> some View {
if #available(iOS 27, tvOS 27, visionOS 27, *) {
content.asyncImageURLSession(urlSession)
} else {
content
}
}
}
2 changes: 1 addition & 1 deletion TopShelf/ContentProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class ContentProvider: TVTopShelfContentProvider {

override func loadTopShelfContent() async -> TVTopShelfContent? {
do {
let api = MediaCCCApiClient()
let api = MediaCCCApiClient(urlSession: .mediaCCCApi)
async let recentTalks = api.recentTalks()
let currentYear = Calendar.current.component(.year, from: .now)
async let popularTalks = api.popularTalks(in: currentYear)
Expand Down
Loading