diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39efde8..b038503 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,8 +103,8 @@ jobs: build-ios: runs-on: macos-latest - env: - TURBO_CACHE_DIR: .turbo/ios + # env: + # TURBO_CACHE_DIR: .turbo/ios steps: - name: Checkout uses: actions/checkout@v3 @@ -112,32 +112,32 @@ jobs: - name: Setup uses: ./.github/actions/setup - - name: Cache turborepo for iOS - uses: actions/cache@v3 - with: - path: ${{ env.TURBO_CACHE_DIR }} - key: ${{ runner.os }}-turborepo-ios-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-turborepo-ios- - - - name: Check turborepo cache for iOS - run: | - TURBO_CACHE_STATUS=$(node -p "($(yarn --silent turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:ios').cache.status") - - if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then - echo "turbo_cache_hit=1" >> $GITHUB_ENV - fi - - - name: Cache cocoapods - if: env.turbo_cache_hit != 1 - id: cocoapods-cache - uses: actions/cache@v3 - with: - path: | - **/ios/Pods - key: ${{ runner.os }}-cocoapods-${{ hashFiles('example/ios/Podfile.lock') }} - restore-keys: | - ${{ runner.os }}-cocoapods- + # - name: Cache turborepo for iOS + # uses: actions/cache@v3 + # with: + # path: ${{ env.TURBO_CACHE_DIR }} + # key: ${{ runner.os }}-turborepo-ios-${{ hashFiles('**/yarn.lock') }} + # restore-keys: | + # ${{ runner.os }}-turborepo-ios- + + # - name: Check turborepo cache for iOS + # run: | + # TURBO_CACHE_STATUS=$(node -p "($(yarn --silent turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" --dry=json)).tasks.find(t => t.task === 'build:ios').cache.status") + + # if [[ $TURBO_CACHE_STATUS == "HIT" ]]; then + # echo "turbo_cache_hit=1" >> $GITHUB_ENV + # fi + + # - name: Cache cocoapods + # if: env.turbo_cache_hit != 1 + # id: cocoapods-cache + # uses: actions/cache@v3 + # with: + # path: | + # **/ios/Pods + # key: ${{ runner.os }}-cocoapods-${{ hashFiles('example/ios/Podfile.lock') }} + # restore-keys: | + # ${{ runner.os }}-cocoapods- - name: Install cocoapods if: env.turbo_cache_hit != 1 && steps.cocoapods-cache.outputs.cache-hit != 'true' @@ -148,4 +148,6 @@ jobs: - name: Build example for iOS run: | - yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" + # yarn turbo run build:ios --cache-dir="${{ env.TURBO_CACHE_DIR }}" + yarn turbo run build:ios + diff --git a/example/ios/TurboImageExample.xcodeproj/project.pbxproj b/example/ios/TurboImageExample.xcodeproj/project.pbxproj index 72ff34e..b70874d 100644 --- a/example/ios/TurboImageExample.xcodeproj/project.pbxproj +++ b/example/ios/TurboImageExample.xcodeproj/project.pbxproj @@ -486,6 +486,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = NO; ENABLE_BITCODE = NO; INFOPLIST_FILE = TurboImageExample/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -513,6 +514,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = NO; INFOPLIST_FILE = TurboImageExample/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/example/src/App.tsx b/example/src/App.tsx index 8328331..ac87d7c 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -3,15 +3,22 @@ import { ScrollView, StyleSheet } from 'react-native'; import TurboImage from 'react-native-turbo-image'; export default function App() { - const image = { source: 'https://picsum.photos/seed/picsum/200/300' }; - const images = Array(100).fill(image); + const image = { source: 'https://placedog.net/300/200?id=12' }; + const images = Array(10).fill(image); return ( {images.map((img, idx) => ( - + ))} ); @@ -26,8 +33,6 @@ const styles = StyleSheet.create({ justifyContent: 'center', }, box: { - width: 300, - height: 200, marginVertical: 20, }, }); diff --git a/ios/TurboImage/RCTConvert+ScaleMode.swift b/ios/TurboImage/RCTConvert+ScaleMode.swift new file mode 100644 index 0000000..ecc5b8c --- /dev/null +++ b/ios/TurboImage/RCTConvert+ScaleMode.swift @@ -0,0 +1,49 @@ +import Foundation +import React +import Kingfisher + +@objc +enum ScaleMode: Int { + + /// Not scale the content. + case none + + /// Scales the content to fit the size of the view by maintaining the aspect ratio. + case aspectFit + + /// Scales the content to fill the size of the view. + case aspectFill +} + +extension ScaleMode { + + static func mapContentMode(by scaleMode: ScaleMode) -> ContentMode { + var contentMode: Kingfisher.ContentMode + switch scaleMode.rawValue { + case 1: + contentMode = .aspectFit + case 2: + contentMode = .aspectFill + default: + contentMode = .none + } + return contentMode + } +} + + +extension RCTConvert { + @objc(ScaleMode:) + static func scaleMode(_ value: Any) -> ScaleMode { + let ScaleModeMap: [String: ScaleMode] = [ + "fill": .aspectFill, + "fit": .aspectFit + ] + + guard let value = value as? String, + let mv = ScaleModeMap[value] + else { return .none } + return mv + } +} + diff --git a/ios/TurboImage/TurboImageView.swift b/ios/TurboImage/TurboImageView.swift index c5892d8..7c708de 100644 --- a/ios/TurboImage/TurboImageView.swift +++ b/ios/TurboImage/TurboImageView.swift @@ -1,22 +1,59 @@ import Kingfisher +import React class TurboImageView : UIView { var image: UIImage? - @objc var color: String = "" { + var imageView: UIImageView? + private var needsReload: Bool = false + + var width: CGFloat? { + didSet { + guard let width = width else { return } + imageView?.frame.size.width = width + } + } + var height: CGFloat? { + didSet { + guard let height = height else { return } + imageView?.frame.size.height = height + } + } + + @objc var source: String? { didSet { - self.backgroundColor = hexStringToUIColor(hexColor: color) + needsReload = true } } - @objc var source: String? + var scaleMode: ScaleMode? override init(frame: CGRect) { super.init(frame: frame) + clipsToBounds = true + imageView = UIImageView() + addSubview(imageView!) + } + + @objc + func setHeight(_ height: CGFloat) { + self.height = height + } + + @objc + func setWidth(_ width: CGFloat) { + self.width = width + } + + @objc + func setScaleMode(_ scaleMode: ScaleMode) { + self.scaleMode = scaleMode } override func didSetProps(_ changedProps: [String]!) { - reloadImage() + if needsReload { + loadImage(with: source) + } } required init?(coder: NSCoder) { @@ -24,36 +61,29 @@ class TurboImageView : UIView { } } + extension TurboImageView { - private func reloadImage() { - guard let url = URL(string: source!) else { return } - let resource: KF.ImageResource = KF.ImageResource(downloadURL: url) - KingfisherManager.shared.retrieveImage(with: resource) { [self] result in - switch result { - case .success(let response): - image = response.image - let imageView = UIImageView(image: image) - addSubview(imageView) - case .failure(let error): - print("🐵 ---- error \(error)") // TODO: 🐵 handle error - } - } - } - - func hexStringToUIColor(hexColor: String) -> UIColor { - let stringScanner = Scanner(string: hexColor) - - if(hexColor.hasPrefix("#")) { - stringScanner.scanLocation = 1 - } - var color: UInt32 = 0 - stringScanner.scanHexInt32(&color) - - let r = CGFloat(Int(color >> 16) & 0x000000FF) - let g = CGFloat(Int(color >> 8) & 0x000000FF) - let b = CGFloat(Int(color) & 0x000000FF) + func loadImage(with source: String?) { + guard let source = source, + let url = URL(string: source), + let width = width, + let height = height, + let scaleMode = scaleMode + else { return } - return UIColor(red: r / 255.0, green: g / 255.0, blue: b / 255.0, alpha: 1) + let resource: KF.ImageResource = KF.ImageResource(downloadURL: url) + let scale = UIScreen.main.scale + let contentMode = ScaleMode.mapContentMode(by: scaleMode) + let referenceSize = CGSize(width: width * scale, height: height * scale) + let processor = ResizingImageProcessor(referenceSize: referenceSize, + mode: contentMode) + let options: KingfisherOptionsInfo = [.processor(processor)] + imageView?.kf.indicatorType = .activity + imageView?.kf.setImage(with: resource, + placeholder: nil, + options: options, + progressBlock: nil + ) } } diff --git a/ios/TurboImage/TurboImageViewManager.m b/ios/TurboImage/TurboImageViewManager.m index e9d85ba..a2496a5 100644 --- a/ios/TurboImage/TurboImageViewManager.m +++ b/ios/TurboImage/TurboImageViewManager.m @@ -1,8 +1,14 @@ #import +#import "ReactNativeTurboImage-umbrella.h" +//#import @interface RCT_EXTERN_MODULE(TurboImageViewManager, RCTViewManager) RCT_EXPORT_VIEW_PROPERTY(color, NSString) RCT_EXPORT_VIEW_PROPERTY(source, NSString) +RCT_EXPORT_VIEW_PROPERTY(width, double) +RCT_EXPORT_VIEW_PROPERTY(height, double) +RCT_EXPORT_VIEW_PROPERTY(scaleMode, ScaleMode) @end + diff --git a/src/index.tsx b/src/index.tsx index 54fdb5d..f6a49c4 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,13 +6,7 @@ import type { ColorValue } from 'react-native'; import type { AccessibilityProps } from 'react-native'; import type { ShadowStyleIOS } from 'react-native'; import type { FlexStyle } from 'react-native'; -import { - View, - StyleSheet, - requireNativeComponent, - UIManager, - Platform, -} from 'react-native'; +import { requireNativeComponent, UIManager, Platform } from 'react-native'; const LINKING_ERROR = `The package 'react-native-turbo-image' doesn't seem to be linked. Make sure: \n\n` + @@ -20,6 +14,19 @@ const LINKING_ERROR = '- You rebuilt the app after installing the package\n' + '- You are not using Expo Go\n'; +export type ScaleMode = 'fit' | 'fill'; +/** + * **aspectFit** + * Scales the content to fit the size of the view by maintaining the aspect ratio. + * + * **aspectFill** + * Scales the content to fill the size of the view. + */ +const scaleMode = { + fit: 'fit', + fill: 'fill', +} as const; + export interface ImageStyle extends FlexStyle, TransformsStyle, ShadowStyleIOS { backfaceVisibility?: 'visible' | 'hidden'; borderBottomLeftRadius?: number; @@ -35,6 +42,8 @@ export interface ImageStyle extends FlexStyle, TransformsStyle, ShadowStyleIOS { } export interface TurboImageProps extends AccessibilityProps, ViewProps { source: string; + ref?: React.Ref; + scaleMode?: ScaleMode; /** * * Style @@ -56,6 +65,8 @@ export interface TurboImageProps extends AccessibilityProps, ViewProps { * Render children within the image. */ children?: React.ReactNode; + width: number; + height: number; } const ComponentName = 'TurboImageView'; @@ -70,27 +81,29 @@ const TurboImageView = const TurboImageBase = ( props: TurboImageProps & { forwardedRef: React.Ref } ) => { - const { source, tintColor, style, children, forwardedRef, ...restProps } = - props; + const { + source, + tintColor, + style, + forwardedRef, + width, + height, + ...restProps + } = props; + return ( - - - {children} - + ); }; -const styles = StyleSheet.create({ - imageContainer: { - overflow: 'hidden', - }, -}); - const TurboImageMemo = memo(TurboImageBase); const TurboImageComponent: React.ComponentType = forwardRef( @@ -99,11 +112,15 @@ const TurboImageComponent: React.ComponentType = forwardRef( ) ); -TurboImageComponent.displayName = 'FastImage'; +TurboImageComponent.displayName = 'TurboImage'; -export interface TurboImageStaticProperties {} +export interface TurboImageStaticProperties { + scaleMode: typeof scaleMode; +} const TurboImage: React.ComponentType & TurboImageStaticProperties = TurboImageComponent as any; +TurboImage.scaleMode = scaleMode; + export default TurboImage;