Skip to content

Commit 0011861

Browse files
committed
feat(mobile): enhance iOS image preview with SDWebImage and improved QuickLook handling
- Add SDWebImage dependency to FollowNative.podspec - Refactor ImagePreview and PreviewControllerController for robust image preview - Implement dynamic file extension handling for image preview - Update WebView bridge to support more flexible image preview payload - Improve MarkdownImage component to use Uint8Array for image conversion Signed-off-by: Innei <tukon479@gmail.com>
1 parent 975285e commit 0011861

File tree

8 files changed

+134
-71
lines changed

8 files changed

+134
-71
lines changed

apps/mobile/native/ios/FollowNative.podspec

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Pod::Spec.new do |s|
2020

2121
s.dependency 'ExpoModulesCore'
2222
s.dependency 'SnapKit', '~> 5.7.0'
23+
s.dependency 'SDWebImage', '~> 5.0'
2324
# Swift/Objective-C compatibility
2425
s.pod_target_xcconfig = {
2526
'DEFINES_MODULE' => 'YES',

apps/mobile/native/ios/Helper/Helper+Image.swift

Lines changed: 70 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,48 +5,78 @@
55
// Created by Innei on 2025/2/7.
66
//
77

8-
import ObjectiveC
98
import QuickLook
9+
import SDWebImage
1010
import UIKit
1111

12-
class PreviewControllerClass: NSObject, QLPreviewControllerDataSource, QLPreviewControllerDelegate {
13-
private var imageDataArray: [Data] = []
12+
private let previewContentDirectory = URL(fileURLWithPath: NSTemporaryDirectory())
13+
.appendingPathComponent(Bundle.main.bundleIdentifier!)
14+
.appendingPathComponent("image-preview")
1415

15-
init(images: [Data]) {
16-
self.imageDataArray = images
17-
super.init()
18-
}
16+
class PreviewControllerController: QLPreviewController, QLPreviewControllerDataSource,
17+
QLPreviewControllerDelegate
18+
{
19+
private var imageDataArray: [Data] = []
20+
private var initialIndex: Int = 0
1921

2022
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
2123
return imageDataArray.count
2224
}
2325

26+
func prepareImages(_ images: [Data], initialIndex: Int = 0) {
27+
self.imageDataArray = images
28+
self.initialIndex = initialIndex
29+
}
30+
2431
func previewController(_ controller: QLPreviewController, previewItemAt index: Int)
2532
-> QLPreviewItem
2633
{
27-
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(
28-
"preview_image_\(index).jpg")
29-
try? imageDataArray[index].write(to: tempURL)
30-
return tempURL as QLPreviewItem
34+
let imageData = imageDataArray[index]
35+
guard let image = UIImage(data: imageData) else {
36+
return previewContentDirectory as QLPreviewItem
37+
}
38+
try? FileManager.default.createDirectory(
39+
at: previewContentDirectory, withIntermediateDirectories: true)
40+
41+
let tempLocation =
42+
previewContentDirectory
43+
.appendingPathComponent(UUID().uuidString)
44+
.appendingPathExtension(image.sd_imageFormat.possiblePathExtension)
45+
46+
try? imageData.write(to: tempLocation)
47+
return tempLocation as QLPreviewItem
3148
}
3249

33-
private func cleanupTempFiles() {
34-
for index in 0..<imageDataArray.count {
35-
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(
36-
"preview_image_\(index).jpg")
37-
try? FileManager.default.removeItem(at: tempURL)
50+
override func viewDidLoad() {
51+
debugPrint(previewContentDirectory, "previewContentDirectory")
52+
53+
super.viewDidLoad()
54+
delegate = self
55+
dataSource = self
56+
view.tintColor = Utils.accentColor
57+
58+
// Set initial preview index
59+
if initialIndex < imageDataArray.count {
60+
self.currentPreviewItemIndex = initialIndex
3861
}
3962
}
4063

64+
private func cleanupTempFiles() {
65+
try? FileManager.default.removeItem(at: previewContentDirectory)
66+
}
67+
4168
func previewControllerDidDismiss(_ controller: QLPreviewController) {
4269
cleanupTempFiles()
4370
}
44-
45-
}
4671

72+
deinit {
73+
self.cleanupTempFiles()
74+
}
75+
76+
}
4777

4878
class ImagePreview: NSObject {
49-
public static func quickLookImage(_ images: [Data]) {
79+
public static func quickLookImage(_ images: [Data], index: Int = 0) {
5080
guard let rootViewController = UIApplication.shared.keyWindow?.rootViewController else {
5181
return
5282
}
@@ -55,12 +85,28 @@ class ImagePreview: NSObject {
5585
print("no preview data")
5686
return
5787
}
58-
let previewController = QLPreviewController()
59-
let previewControllerClass = PreviewControllerClass(images: images)
60-
previewController.view.tintColor = Utils.accentColor
61-
previewController.dataSource = previewControllerClass
62-
previewController.delegate = previewControllerClass
88+
89+
let previewController = PreviewControllerController()
90+
previewController.prepareImages(images, initialIndex: index)
6391

6492
rootViewController.present(previewController, animated: true)
6593
}
6694
}
95+
96+
extension SDImageFormat {
97+
var possiblePathExtension: String {
98+
switch self {
99+
case .undefined: ""
100+
case .JPEG: "jpg"
101+
case .PNG: "png"
102+
case .GIF: "gif"
103+
case .TIFF: "tiff"
104+
case .webP: "webp"
105+
case .HEIC: "heic"
106+
case .HEIF: "heif"
107+
case .PDF: "pdf"
108+
case .SVG: "svg"
109+
default: "png"
110+
}
111+
}
112+
}

apps/mobile/native/ios/SharedWebView/CustomURLSchemeHandler.swift

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55
// Created by Innei on 2025/2/7.
66
//
77

8-
import WebKit
98
import Foundation
10-
9+
import WebKit
1110

1211
class CustomURLSchemeHandler: NSObject, WKURLSchemeHandler {
1312
static let rewriteScheme = "follow-xhr"
@@ -17,7 +16,7 @@ class CustomURLSchemeHandler: NSObject, WKURLSchemeHandler {
1716
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
1817
guard let url = urlSchemeTask.request.url,
1918
let originalURLString = url.absoluteString.replacingOccurrences(
20-
of: CustomURLSchemeHandler.rewriteScheme, with: "https"
19+
of: CustomURLSchemeHandler.rewriteScheme, with: "https"
2120
).removingPercentEncoding,
2221
let originalURL = URL(string: originalURLString)
2322
else {
@@ -48,28 +47,28 @@ class CustomURLSchemeHandler: NSObject, WKURLSchemeHandler {
4847
let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
4948
guard let self = self else { return }
5049

51-
self.queue.sync {
52-
// Check if task is still active
53-
guard self.activeTasks[taskID] != nil else { return }
50+
// Check if task is still active
51+
guard self.activeTasks[taskID] != nil else { return }
5452

55-
if let error = error {
56-
urlSchemeTask.didFailWithError(error)
53+
if let error = error {
54+
urlSchemeTask.didFailWithError(error)
55+
self.queue.sync {
5756
self.activeTasks.removeValue(forKey: taskID)
58-
return
5957
}
58+
return
59+
}
6060

61-
if let response = response as? HTTPURLResponse, let data = data {
62-
do {
63-
urlSchemeTask.didReceive(response)
64-
urlSchemeTask.didReceive(data)
65-
urlSchemeTask.didFinish()
66-
} catch {
67-
// Ignore errors that might occur if task was stopped
68-
print("Error completing URL scheme task: \(error)")
69-
}
61+
if let response = response as? HTTPURLResponse, let data = data {
62+
do {
63+
urlSchemeTask.didReceive(response)
64+
urlSchemeTask.didReceive(data)
65+
urlSchemeTask.didFinish()
66+
} catch {
67+
// Ignore errors that might occur if task was stopped
68+
print("Error completing URL scheme task: \(error)")
7069
}
71-
self.activeTasks.removeValue(forKey: taskID)
7270
}
71+
self.activeTasks.removeValue(forKey: taskID)
7372
}
7473
queue.sync {
7574
activeTasks[taskID] = task

apps/mobile/native/ios/SharedWebView/Injected/at_start.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,15 @@
2323
payload: height,
2424
})
2525
},
26-
previewImage: (base64) => {
26+
previewImage: (data) => {
2727
send({
2828
type: "previewImage",
29-
payload: base64,
29+
payload: {
30+
images: data.images.map((image) => Array.from(image)),
31+
index: data.index,
32+
ext: data.ext,
33+
filename: data.filename,
34+
},
3035
})
3136
},
3237
}

apps/mobile/native/ios/SharedWebView/SharedWebView+BridgeData.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,21 @@ private protocol BasePayload {
1010
var type: String { get }
1111
}
1212

13-
struct SetContentHeightPayload: Hashable, Codable, BasePayload {
13+
struct SetContentHeightPayload: Codable, BasePayload {
1414
var type: String
1515
var payload: CGFloat
1616
}
1717

18-
struct BridgeDataBasePayload: Hashable, Codable {
18+
struct BridgeDataBasePayload: Codable {
1919
var type: String
2020
}
2121

22-
struct PreviewImagePayload: Hashable, Codable, BasePayload {
22+
struct PreviewImagePayloadProps: Codable {
23+
var images: [[UInt8]]
24+
var index: Int = 0
25+
}
26+
27+
struct PreviewImagePayload: Codable, BasePayload {
2328
var type: String
24-
var payload: [String]
29+
var payload: PreviewImagePayloadProps
2530
}

apps/mobile/native/ios/SharedWebView/WebViewManager.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ private class WebViewDelegate: NSObject, WKNavigationDelegate, WKScriptMessageHa
219219
guard let data = data else { return }
220220
DispatchQueue.main.async {
221221
ImagePreview.quickLookImage(
222-
data.payload.compactMap { Data(base64Encoded: $0) })
222+
data.payload.images.compactMap { Data($0) })
223223
}
224224

225225
default:

apps/mobile/web-app/html-renderer/global.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import "../../../../packages/types/global"
55
interface Bridge {
66
measure: () => void
77
setContentHeight: (height: number) => void
8-
previewImage: (base64: string[]) => void
8+
previewImage: (data: { images: Uint8Array[]; index: number }) => void
99
}
1010

1111
declare global {
Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,44 @@
1+
import { useRef } from "react"
2+
13
import type { HTMLProps } from "~/HTML"
24

35
export const MarkdownImage = (props: HTMLProps<"img">) => {
46
const { src, ...rest } = props
57

8+
const imageRef = useRef<HTMLImageElement>(null)
9+
610
return (
711
<button
812
type="button"
913
onClick={() => {
10-
if (!src) return
11-
const $image = new Image()
12-
$image.src = src
13-
$image.crossOrigin = "anonymous"
14-
15-
$image.onload = () => {
16-
// Create a canvas element
17-
const canvas = document.createElement("canvas")
18-
canvas.width = $image.width
19-
canvas.height = $image.height
14+
const $image = imageRef.current
15+
if (!$image) return
16+
const canvas = document.createElement("canvas")
17+
canvas.width = $image.naturalWidth
18+
canvas.height = $image.naturalHeight
2019

21-
// Draw image on canvas
22-
const ctx = canvas.getContext("2d")
23-
ctx?.drawImage($image, 0, 0)
20+
// Draw image on canvas
21+
const ctx = canvas.getContext("2d")
22+
if (!ctx) return
23+
ctx.drawImage($image, 0, 0)
2424

25-
// Convert to base64
26-
const imageBase64 = canvas.toDataURL("image/png")
25+
canvas.toBlob((blob) => {
26+
if (!blob) return
27+
const reader = new FileReader()
28+
// eslint-disable-next-line unicorn/prefer-blob-reading-methods
29+
reader.readAsArrayBuffer(blob)
2730

28-
// Remove base64 prefix
29-
const base64 = imageBase64.split(",")[1]!
30-
bridge.previewImage([base64])
31-
}
31+
reader.onloadend = () => {
32+
const uint8Array = new Uint8Array(reader.result as ArrayBuffer)
33+
bridge.previewImage({
34+
images: [uint8Array],
35+
index: 0,
36+
})
37+
}
38+
}, "image/png")
3239
}}
3340
>
34-
<img {...rest} src={src} />
41+
<img {...rest} crossOrigin="anonymous" src={src} ref={imageRef} />
3542
</button>
3643
)
3744
}

0 commit comments

Comments
 (0)