Skip to content

Commit

Permalink
feat(mobile): enhance iOS image preview with SDWebImage and improved …
Browse files Browse the repository at this point in the history
…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 <[email protected]>
  • Loading branch information
Innei committed Feb 8, 2025
1 parent 975285e commit 0011861
Show file tree
Hide file tree
Showing 8 changed files with 134 additions and 71 deletions.
1 change: 1 addition & 0 deletions apps/mobile/native/ios/FollowNative.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Pod::Spec.new do |s|

s.dependency 'ExpoModulesCore'
s.dependency 'SnapKit', '~> 5.7.0'
s.dependency 'SDWebImage', '~> 5.0'
# Swift/Objective-C compatibility
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
Expand Down
94 changes: 70 additions & 24 deletions apps/mobile/native/ios/Helper/Helper+Image.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,78 @@
// Created by Innei on 2025/2/7.
//

import ObjectiveC
import QuickLook
import SDWebImage
import UIKit

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

init(images: [Data]) {
self.imageDataArray = images
super.init()
}
class PreviewControllerController: QLPreviewController, QLPreviewControllerDataSource,
QLPreviewControllerDelegate
{
private var imageDataArray: [Data] = []
private var initialIndex: Int = 0

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

func prepareImages(_ images: [Data], initialIndex: Int = 0) {
self.imageDataArray = images
self.initialIndex = initialIndex
}

func previewController(_ controller: QLPreviewController, previewItemAt index: Int)
-> QLPreviewItem
{
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(
"preview_image_\(index).jpg")
try? imageDataArray[index].write(to: tempURL)
return tempURL as QLPreviewItem
let imageData = imageDataArray[index]
guard let image = UIImage(data: imageData) else {
return previewContentDirectory as QLPreviewItem
}
try? FileManager.default.createDirectory(
at: previewContentDirectory, withIntermediateDirectories: true)

let tempLocation =
previewContentDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension(image.sd_imageFormat.possiblePathExtension)

try? imageData.write(to: tempLocation)
return tempLocation as QLPreviewItem
}

private func cleanupTempFiles() {
for index in 0..<imageDataArray.count {
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(
"preview_image_\(index).jpg")
try? FileManager.default.removeItem(at: tempURL)
override func viewDidLoad() {
debugPrint(previewContentDirectory, "previewContentDirectory")

super.viewDidLoad()
delegate = self
dataSource = self
view.tintColor = Utils.accentColor

// Set initial preview index
if initialIndex < imageDataArray.count {
self.currentPreviewItemIndex = initialIndex
}
}

private func cleanupTempFiles() {
try? FileManager.default.removeItem(at: previewContentDirectory)
}

func previewControllerDidDismiss(_ controller: QLPreviewController) {
cleanupTempFiles()
}

}

deinit {
self.cleanupTempFiles()
}

}

class ImagePreview: NSObject {
public static func quickLookImage(_ images: [Data]) {
public static func quickLookImage(_ images: [Data], index: Int = 0) {
guard let rootViewController = UIApplication.shared.keyWindow?.rootViewController else {
return
}
Expand All @@ -55,12 +85,28 @@ class ImagePreview: NSObject {
print("no preview data")
return
}
let previewController = QLPreviewController()
let previewControllerClass = PreviewControllerClass(images: images)
previewController.view.tintColor = Utils.accentColor
previewController.dataSource = previewControllerClass
previewController.delegate = previewControllerClass

let previewController = PreviewControllerController()
previewController.prepareImages(images, initialIndex: index)

rootViewController.present(previewController, animated: true)
}
}

extension SDImageFormat {
var possiblePathExtension: String {
switch self {
case .undefined: ""
case .JPEG: "jpg"
case .PNG: "png"
case .GIF: "gif"
case .TIFF: "tiff"
case .webP: "webp"
case .HEIC: "heic"
case .HEIF: "heif"
case .PDF: "pdf"
case .SVG: "svg"
default: "png"
}
}
}
37 changes: 18 additions & 19 deletions apps/mobile/native/ios/SharedWebView/CustomURLSchemeHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
// Created by Innei on 2025/2/7.
//

import WebKit
import Foundation

import WebKit

class CustomURLSchemeHandler: NSObject, WKURLSchemeHandler {
static let rewriteScheme = "follow-xhr"
Expand All @@ -17,7 +16,7 @@ class CustomURLSchemeHandler: NSObject, WKURLSchemeHandler {
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
guard let url = urlSchemeTask.request.url,
let originalURLString = url.absoluteString.replacingOccurrences(
of: CustomURLSchemeHandler.rewriteScheme, with: "https"
of: CustomURLSchemeHandler.rewriteScheme, with: "https"
).removingPercentEncoding,
let originalURL = URL(string: originalURLString)
else {
Expand Down Expand Up @@ -48,28 +47,28 @@ class CustomURLSchemeHandler: NSObject, WKURLSchemeHandler {
let task = URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
guard let self = self else { return }

self.queue.sync {
// Check if task is still active
guard self.activeTasks[taskID] != nil else { return }
// Check if task is still active
guard self.activeTasks[taskID] != nil else { return }

if let error = error {
urlSchemeTask.didFailWithError(error)
if let error = error {
urlSchemeTask.didFailWithError(error)
self.queue.sync {
self.activeTasks.removeValue(forKey: taskID)
return
}
return
}

if let response = response as? HTTPURLResponse, let data = data {
do {
urlSchemeTask.didReceive(response)
urlSchemeTask.didReceive(data)
urlSchemeTask.didFinish()
} catch {
// Ignore errors that might occur if task was stopped
print("Error completing URL scheme task: \(error)")
}
if let response = response as? HTTPURLResponse, let data = data {
do {
urlSchemeTask.didReceive(response)
urlSchemeTask.didReceive(data)
urlSchemeTask.didFinish()
} catch {
// Ignore errors that might occur if task was stopped
print("Error completing URL scheme task: \(error)")
}
self.activeTasks.removeValue(forKey: taskID)
}
self.activeTasks.removeValue(forKey: taskID)
}
queue.sync {
activeTasks[taskID] = task
Expand Down
9 changes: 7 additions & 2 deletions apps/mobile/native/ios/SharedWebView/Injected/at_start.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,15 @@
payload: height,
})
},
previewImage: (base64) => {
previewImage: (data) => {
send({
type: "previewImage",
payload: base64,
payload: {
images: data.images.map((image) => Array.from(image)),
index: data.index,
ext: data.ext,
filename: data.filename,
},
})
},
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,21 @@ private protocol BasePayload {
var type: String { get }
}

struct SetContentHeightPayload: Hashable, Codable, BasePayload {
struct SetContentHeightPayload: Codable, BasePayload {
var type: String
var payload: CGFloat
}

struct BridgeDataBasePayload: Hashable, Codable {
struct BridgeDataBasePayload: Codable {
var type: String
}

struct PreviewImagePayload: Hashable, Codable, BasePayload {
struct PreviewImagePayloadProps: Codable {
var images: [[UInt8]]
var index: Int = 0
}

struct PreviewImagePayload: Codable, BasePayload {
var type: String
var payload: [String]
var payload: PreviewImagePayloadProps
}
2 changes: 1 addition & 1 deletion apps/mobile/native/ios/SharedWebView/WebViewManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ private class WebViewDelegate: NSObject, WKNavigationDelegate, WKScriptMessageHa
guard let data = data else { return }
DispatchQueue.main.async {
ImagePreview.quickLookImage(
data.payload.compactMap { Data(base64Encoded: $0) })
data.payload.images.compactMap { Data($0) })
}

default:
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/web-app/html-renderer/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import "../../../../packages/types/global"
interface Bridge {
measure: () => void
setContentHeight: (height: number) => void
previewImage: (base64: string[]) => void
previewImage: (data: { images: Uint8Array[]; index: number }) => void
}

declare global {
Expand Down
47 changes: 27 additions & 20 deletions apps/mobile/web-app/html-renderer/src/components/image.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,44 @@
import { useRef } from "react"

import type { HTMLProps } from "~/HTML"

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

const imageRef = useRef<HTMLImageElement>(null)

return (
<button
type="button"
onClick={() => {
if (!src) return
const $image = new Image()
$image.src = src
$image.crossOrigin = "anonymous"

$image.onload = () => {
// Create a canvas element
const canvas = document.createElement("canvas")
canvas.width = $image.width
canvas.height = $image.height
const $image = imageRef.current
if (!$image) return
const canvas = document.createElement("canvas")
canvas.width = $image.naturalWidth
canvas.height = $image.naturalHeight

// Draw image on canvas
const ctx = canvas.getContext("2d")
ctx?.drawImage($image, 0, 0)
// Draw image on canvas
const ctx = canvas.getContext("2d")
if (!ctx) return
ctx.drawImage($image, 0, 0)

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

// Remove base64 prefix
const base64 = imageBase64.split(",")[1]!
bridge.previewImage([base64])
}
reader.onloadend = () => {
const uint8Array = new Uint8Array(reader.result as ArrayBuffer)
bridge.previewImage({
images: [uint8Array],
index: 0,
})
}
}, "image/png")
}}
>
<img {...rest} src={src} />
<img {...rest} crossOrigin="anonymous" src={src} ref={imageRef} />
</button>
)
}

0 comments on commit 0011861

Please sign in to comment.