Skip to content

Commit 84cd465

Browse files
authored
fix: Allow caching local images with zero lag (#95)
* fix: Fix scale for local images * fix: Fix recycling not appearing back * Update package.json * fix: Allow caching local images in-memory with zero lag * Update DispatchQueue+runOnMain.swift * chore: Lint * fix: Fix types
1 parent f216eba commit 84cd465

8 files changed

Lines changed: 1119 additions & 1152 deletions

File tree

bun.lock

Lines changed: 1039 additions & 1107 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/src/NitroImageTab.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ export function NitroImageTab() {
1515
data={imageURLs}
1616
renderItem={({ item: url }) => (
1717
<NitroImage
18-
image={{ url: url }}
18+
image={{ url: url }}
1919
style={styles.image}
20-
resizeMode="cover" />
20+
resizeMode="cover" />
2121
)}
2222
/>
2323
</View>

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-native-nitro-image-monorepo",
3-
"packageManager": "bun@1.3.6",
3+
"packageManager": "bun@1.3.9",
44
"private": true,
55
"version": "0.10.2",
66
"repository": "https://github.com/mrousavy/react-native-nitro-image.git",

packages/react-native-nitro-image/ios/CustomImageView.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ internal class CustomImageView: UIImageView {
2727
fatalError("init(coder:) has not been implemented")
2828
}
2929

30-
override func willMove(toSuperview newSuperview: UIView?) {
31-
super.willMove(toSuperview: newSuperview)
32-
onVisibilityChanged(isVisible: newSuperview != nil)
30+
override func willMove(toWindow newWindow: UIWindow?) {
31+
super.willMove(toWindow: newWindow)
32+
onVisibilityChanged(isVisible: newWindow != nil)
3333
}
3434
private func onVisibilityChanged(isVisible: Bool) {
3535
if isVisible {
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//
2+
// DispatchQueue+runOnMain.swift
3+
// react-native-nitro-image
4+
//
5+
// Created by Marc Rousavy on 10.02.26.
6+
//
7+
8+
import Foundation
9+
10+
extension DispatchQueue {
11+
static func runOnMain(_ block: @escaping () -> Void) {
12+
if Thread.isMainThread {
13+
block()
14+
} else {
15+
DispatchQueue.main.async(execute: block)
16+
}
17+
}
18+
}

packages/react-native-nitro-image/ios/HybridImageLoader.swift

Lines changed: 52 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -11,59 +11,75 @@ class HybridImageLoader: HybridImageLoaderSpec {
1111
typealias LoadFunc = () throws -> Promise<any HybridImageSpec>
1212
typealias RequestFunc = (_ for: any HybridNitroImageViewSpec) throws -> Void
1313
typealias DropFunc = (_ for: any HybridNitroImageViewSpec) throws -> Void
14-
14+
1515
private let load: LoadFunc
16-
private let requestImage: RequestFunc
17-
private let dropImage: DropFunc
18-
19-
init(load: @escaping LoadFunc) {
16+
private let requestImage: RequestFunc?
17+
private let dropImage: DropFunc?
18+
private let allowCaching: Bool
19+
private var cachedResult: (any HybridImageSpec)? = nil
20+
21+
init(load: @escaping LoadFunc, allowCaching: Bool = true) {
2022
self.load = load
21-
self.requestImage = Self.defaultRequestFunc(forLoadFunc: load)
22-
self.dropImage = Self.defaultDropFunc()
23+
self.requestImage = nil
24+
self.dropImage = nil
25+
self.allowCaching = allowCaching
2326
}
2427
init(load: @escaping LoadFunc,
2528
requestImage: @escaping RequestFunc,
26-
dropImage: @escaping DropFunc) {
29+
dropImage: @escaping DropFunc,
30+
allowCaching: Bool = true) {
2731
self.load = load
2832
self.requestImage = requestImage
2933
self.dropImage = dropImage
34+
self.allowCaching = allowCaching
3035
}
31-
32-
func loadImage() throws -> Promise<any HybridImageSpec> {
33-
return try load()
34-
}
35-
36-
func requestImage(forView view: any HybridNitroImageViewSpec) throws {
37-
return try requestImage(view)
36+
37+
func dispose() {
38+
self.cachedResult = nil
3839
}
39-
40-
func dropImage(forView view: any HybridNitroImageViewSpec) throws {
41-
return try dropImage(view)
40+
41+
func loadImage() throws -> Promise<any HybridImageSpec> {
42+
if allowCaching {
43+
// We can cache the last loaded image in state, so future requests receive it instantly
44+
if let cachedResult {
45+
return .resolved(withResult: cachedResult)
46+
}
47+
return try load()
48+
.then { [weak self] image in
49+
guard let self else { return }
50+
self.cachedResult = image
51+
}
52+
} else {
53+
// We need to reload the Image each time.
54+
return try load()
55+
}
4256
}
43-
}
4457

45-
fileprivate extension HybridImageLoader {
46-
static func defaultRequestFunc(forLoadFunc loadFunc: @escaping LoadFunc) -> RequestFunc {
47-
return { view in
58+
func requestImage(forView view: any HybridNitroImageViewSpec) throws {
59+
if let requestImage {
60+
// Custom requestImage(...) func supplied
61+
return try requestImage(view)
62+
} else {
63+
// Default: Load & set image.
4864
guard let view = view as? NativeImageView else { return }
49-
let promise = try loadFunc()
50-
promise.then { image in
51-
guard let image = image as? NativeImage else { return }
52-
Task { @MainActor in
53-
view.imageView.image = image.uiImage
65+
try loadImage()
66+
.then { image in
67+
guard let image = image as? NativeImage else { return }
68+
DispatchQueue.runOnMain {
69+
view.imageView.image = image.uiImage
70+
}
5471
}
55-
}
56-
promise.catch { _ in
57-
Task { @MainActor in
58-
view.imageView.image = nil
59-
}
60-
}
6172
}
6273
}
63-
static func defaultDropFunc() -> DropFunc {
64-
return { view in
74+
75+
func dropImage(forView view: any HybridNitroImageViewSpec) throws {
76+
if let dropImage {
77+
// Custom dropImage(...) func supplied
78+
return try dropImage(view)
79+
} else {
80+
// Default: Set image to nil
6581
guard let view = view as? NativeImageView else { return }
66-
Task { @MainActor in
82+
DispatchQueue.runOnMain {
6783
view.imageView.image = nil
6884
}
6985
}

packages/react-native-nitro-image/ios/HybridImageView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ class HybridImageView: HybridNitroImageViewSpec {
2020

2121
var resizeMode: ResizeMode? {
2222
didSet {
23-
Task { @MainActor in
23+
DispatchQueue.runOnMain {
2424
self.updateResizeMode()
2525
}
2626
}
2727
}
2828
var image: (Variant__any_HybridImageSpec___any_HybridImageLoaderSpec_)? = nil {
2929
didSet {
30-
Task { @MainActor in
30+
DispatchQueue.runOnMain {
3131
self.updateImage()
3232
}
3333
}

packages/react-native-nitro-image/src/OptionalWebLoader.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// biome-ignore lint/suspicious/noTsIgnore: Type Compilation is a race-condition
22
// @ts-ignore
33
type WebImagesType = typeof import("react-native-nitro-web-image")["WebImages"];
4+
type OptionalWebImagesType = Pick<WebImagesType, 'createWebImageLoader' | 'loadFromURLAsync'>
45

56
let createWebImageLoader: WebImagesType["createWebImageLoader"] = () => {
67
throw new Error(
@@ -26,4 +27,4 @@ try {
2627
// react-native-nitro-web-image is not installed, so only local images are supported.
2728
}
2829

29-
export const OptionalWebImages = { createWebImageLoader, loadFromURLAsync };
30+
export const OptionalWebImages: OptionalWebImagesType = { createWebImageLoader, loadFromURLAsync };

0 commit comments

Comments
 (0)