Skip to content

Commit 929a4c1

Browse files
feat(iOS): async image loading (#318)
<!-- Thanks for submitting a pull request! We appreciate you spending the time to work on these changes. Please follow the template so that the reviewers can easily understand what the code changes affect --> # Summary Currently, all images on iOS are loaded in sync way. It fully blocks the main thread and lets's say if we have a large image user has to wait until this image is loaded. The best approach for images is to always load them asynchronously. If it's a local image -> it will be immediately loaded, it won't block the main thread. Additionally, I added the MediaAttachment class that implements the abstract presentation of all attachments. So, if in the future if you would like to add video/block image attachments, it would be easier to extend from this class ## Test Plan Test case 1: 1. Insert any image manually Test case 2: 1. set the initial html like: ``` <html> <p><img src="https://picsum.photos/id/1/200/200" width="200.000000" height="200.000000"/></p> <p><img src="https://picsum.photos/id/2/200/200" width="200.000000" height="200.000000"/></p> <p><img src="https://picsum.photos/id/3/200/200" width="200.000000" height="200.000000"/></p> <p><img src="https://picsum.photos/id/4/200/200" width="200.000000" height="200.000000"/></p> <p><img src="https://picsum.photos/id/5/200/200" width="200.000000" height="200.000000"/></p> <p><img src="https://picsum.photos/id/6/200/200" width="200.000000" height="200.000000"/></p> <p><img src="https://picsum.photos/id/7/200/200" width="200.000000" height="200.000000"/></p> <p><img src="https://picsum.photos/id/8/200/200" width="200.000000" height="200.000000"/></p> <p><img src="https://picsum.photos/id/9/200/200" width="200.000000" height="200.000000"/></p> <p><img src="https://picsum.photos/id/10/200/200" width="200.000000" height="200.000000"/></p> <p><img src="https://picsum.photos/id/11/200/200" width="200.000000" height="200.000000"/></p> <p><img src="https://picsum.photos/id/12/200/200" width="200.000000" height="200.000000"/></p> <p><img src="https://picsum.photos/id/13/200/200" width="200.000000" height="200.000000"/></p> <p><img src="https://picsum.photos/id/14/200/200" width="200.000000" height="200.000000"/></p> <p><img src="https://picsum.photos/id/15/200/200" width="200.000000" height="200.000000"/></p> <p><img src="https://picsum.photos/id/16/200/200" width="200.000000" height="200.000000"/></p> <p><img src="https://picsum.photos/id/17/200/200" width="200.000000" height="200.000000"/></p> <p><img src="https://picsum.photos/id/18/200/200" width="200.000000" height="200.000000"/></p> <p><img src="https://picsum.photos/id/19/200/200" width="200.000000" height="200.000000"/></p> <p><img src="https://picsum.photos/id/20/200/200" width="200.000000" height="200.000000"/></p> </html> ``` ## Screenshots / Videos ### Before: https://github.com/user-attachments/assets/f2a60c27-6d1a-4cd8-ba56-b44d2f9813be ### After: https://github.com/user-attachments/assets/837aa5c9-aa35-49c7-ada2-a18b9b4116da https://github.com/user-attachments/assets/1c32ccc6-fc5a-4caa-8225-5770122e502c Include any visual proof that helps reviewers understand the change — UI updates, bug reproduction or the result of the fix. ## Compatibility | OS | Implemented | | ------- | :---------: | | iOS | ✅ | | Android | ❌ | --------- Co-authored-by: Mikołaj Szydłowski <9szydlowski9@gmail.com>
1 parent d68eda4 commit 929a4c1

File tree

7 files changed

+141
-30
lines changed

7 files changed

+141
-30
lines changed

ios/EnrichedTextInputView.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#import "InputConfig.h"
44
#import "InputParser.h"
55
#import "InputTextView.h"
6+
#import "MediaAttachment.h"
67
#import <React/RCTViewComponentView.h>
78
#import <UIKit/UIKit.h>
89

@@ -11,7 +12,8 @@
1112

1213
NS_ASSUME_NONNULL_BEGIN
1314

14-
@interface EnrichedTextInputView : RCTViewComponentView {
15+
@interface EnrichedTextInputView
16+
: RCTViewComponentView <MediaAttachmentDelegate> {
1517
@public
1618
InputTextView *textView;
1719
@public

ios/EnrichedTextInputView.mm

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1558,4 +1558,31 @@ - (void)textViewDidChange:(UITextView *)textView {
15581558
[self anyTextMayHaveBeenModified];
15591559
}
15601560

1561+
// MARK: - Media attachments delegate
1562+
1563+
- (void)mediaAttachmentDidUpdate:(NSTextAttachment *)attachment {
1564+
NSTextStorage *storage = textView.textStorage;
1565+
NSRange fullRange = NSMakeRange(0, storage.length);
1566+
1567+
__block NSRange foundRange = NSMakeRange(NSNotFound, 0);
1568+
1569+
[storage enumerateAttribute:NSAttachmentAttributeName
1570+
inRange:fullRange
1571+
options:0
1572+
usingBlock:^(id value, NSRange range, BOOL *stop) {
1573+
if (value == attachment) {
1574+
foundRange = range;
1575+
*stop = YES;
1576+
}
1577+
}];
1578+
1579+
if (foundRange.location == NSNotFound) {
1580+
return;
1581+
}
1582+
1583+
[storage edited:NSTextStorageEditedAttributes
1584+
range:foundRange
1585+
changeInLength:0];
1586+
}
1587+
15611588
@end

ios/attachments/ImageAttachment.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#import "ImageData.h"
2+
#import "MediaAttachment.h"
3+
4+
@interface ImageAttachment : MediaAttachment
5+
6+
@property(nonatomic, strong) ImageData *imageData;
7+
8+
- (instancetype)initWithImageData:(ImageData *)data;
9+
10+
@end

ios/attachments/ImageAttachment.mm

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#import "ImageAttachment.h"
2+
3+
@implementation ImageAttachment
4+
5+
- (instancetype)initWithImageData:(ImageData *)data {
6+
self = [super initWithURI:data.uri width:data.width height:data.height];
7+
if (!self)
8+
return nil;
9+
10+
_imageData = data;
11+
self.image = [UIImage new];
12+
13+
[self loadAsync];
14+
return self;
15+
}
16+
17+
- (void)loadAsync {
18+
NSURL *url = [NSURL URLWithString:self.uri];
19+
if (!url)
20+
return;
21+
22+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
23+
NSData *bytes = [NSData dataWithContentsOfURL:url];
24+
UIImage *img = bytes ? [UIImage imageWithData:bytes]
25+
: [UIImage systemImageNamed:@"file"];
26+
27+
dispatch_async(dispatch_get_main_queue(), ^{
28+
self.image = img;
29+
[self notifyUpdate];
30+
});
31+
});
32+
}
33+
34+
@end

ios/attachments/MediaAttachment.h

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#import <UIKit/UIKit.h>
2+
3+
@class MediaAttachment;
4+
5+
@protocol MediaAttachmentDelegate <NSObject>
6+
- (void)mediaAttachmentDidUpdate:(MediaAttachment *)attachment;
7+
@end
8+
9+
@interface MediaAttachment : NSTextAttachment
10+
11+
@property(nonatomic, weak) id<MediaAttachmentDelegate> delegate;
12+
@property(nonatomic, strong) NSString *uri;
13+
@property(nonatomic, assign) CGFloat width;
14+
@property(nonatomic, assign) CGFloat height;
15+
16+
- (instancetype)initWithURI:(NSString *)uri
17+
width:(CGFloat)width
18+
height:(CGFloat)height;
19+
20+
- (void)loadAsync;
21+
- (void)notifyUpdate;
22+
23+
@end

ios/attachments/MediaAttachment.mm

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#import "MediaAttachment.h"
2+
3+
@implementation MediaAttachment
4+
5+
- (instancetype)initWithURI:(NSString *)uri
6+
width:(CGFloat)width
7+
height:(CGFloat)height {
8+
self = [super init];
9+
if (!self)
10+
return nil;
11+
12+
_uri = uri;
13+
_width = width;
14+
_height = height;
15+
16+
self.bounds = CGRectMake(0, 0, width, height);
17+
18+
return self;
19+
}
20+
21+
- (void)loadAsync {
22+
// no-op for base
23+
}
24+
25+
- (void)notifyUpdate {
26+
if ([self.delegate respondsToSelector:@selector(mediaAttachmentDidUpdate:)]) {
27+
[self.delegate mediaAttachmentDidUpdate:self];
28+
}
29+
}
30+
31+
@end

ios/styles/ImageStyle.mm

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
#import "EnrichedTextInputView.h"
2+
#import "ImageAttachment.h"
3+
#import "MediaAttachment.h"
24
#import "OccurenceUtils.h"
35
#import "StyleHeaders.h"
46
#import "TextInsertionUtils.h"
@@ -115,27 +117,28 @@ - (ImageData *)getImageDataAt:(NSUInteger)location {
115117
- (void)addImageAtRange:(NSRange)range
116118
imageData:(ImageData *)imageData
117119
withSelection:(BOOL)withSelection {
118-
UIImage *img = [self prepareImageFromUri:imageData.uri];
120+
if (!imageData)
121+
return;
119122

120-
NSDictionary *attributes = [@{
121-
NSAttachmentAttributeName : [self prepareImageAttachement:img
122-
width:imageData.width
123-
height:imageData.height],
124-
ImageAttributeName : imageData,
125-
} mutableCopy];
123+
ImageAttachment *attachment =
124+
[[ImageAttachment alloc] initWithImageData:imageData];
125+
attachment.delegate = _input;
126+
127+
NSDictionary *attributes =
128+
@{NSAttachmentAttributeName : attachment, ImageAttributeName : imageData};
126129

127130
// Use the Object Replacement Character for Image.
128131
// This tells TextKit "something non-text goes here".
129-
NSString *imagePlaceholder = @"\uFFFC";
132+
NSString *placeholderChar = @"\uFFFC";
130133

131134
if (range.length == 0) {
132-
[TextInsertionUtils insertText:imagePlaceholder
135+
[TextInsertionUtils insertText:placeholderChar
133136
at:range.location
134137
additionalAttributes:attributes
135138
input:_input
136139
withSelection:withSelection];
137140
} else {
138-
[TextInsertionUtils replaceText:imagePlaceholder
141+
[TextInsertionUtils replaceText:placeholderChar
139142
at:range
140143
additionalAttributes:attributes
141144
input:_input
@@ -154,23 +157,4 @@ - (void)addImage:(NSString *)uri width:(CGFloat)width height:(CGFloat)height {
154157
withSelection:YES];
155158
}
156159

157-
- (NSTextAttachment *)prepareImageAttachement:(UIImage *)image
158-
width:(CGFloat)width
159-
height:(CGFloat)height {
160-
161-
NSTextAttachment *attachment = [[NSTextAttachment alloc] init];
162-
attachment.image = image;
163-
attachment.bounds = CGRectMake(0, 0, width, height);
164-
165-
return attachment;
166-
}
167-
168-
- (UIImage *)prepareImageFromUri:(NSString *)uri {
169-
NSURL *url = [NSURL URLWithString:uri];
170-
NSData *imgData = [NSData dataWithContentsOfURL:url];
171-
UIImage *image = [UIImage imageWithData:imgData];
172-
173-
return image;
174-
}
175-
176160
@end

0 commit comments

Comments
 (0)