Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/database.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
uses: actions/checkout@main
- name: Select Xcode version
run: |
sudo xcode-select -switch /Applications/Xcode_16.3.app/Contents/Developer
sudo xcode-select -switch /Applications/Xcode_16.4.app/Contents/Developer
- name: Setup
run: |
cd FirebaseDatabaseUI
Expand All @@ -50,7 +50,7 @@ jobs:
uses: actions/checkout@main
- name: Select Xcode version
run: |
sudo xcode-select -switch /Applications/Xcode_16.3.app/Contents/Developer
sudo xcode-select -switch /Applications/Xcode_16.4.app/Contents/Developer
- name: List
run: |
xcodebuild -list
Expand All @@ -66,7 +66,7 @@ jobs:
uses: actions/checkout@main
- name: Select Xcode version
run: |
sudo xcode-select -switch /Applications/Xcode_16.3.app/Contents/Developer
sudo xcode-select -switch /Applications/Xcode_16.4.app/Contents/Developer
- name: Setup
run: gem install bundler; bundle install
- name: Build
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/firestore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
uses: actions/checkout@main
- name: Select Xcode version
run: |
sudo xcode-select -switch /Applications/Xcode_16.3.app/Contents/Developer
sudo xcode-select -switch /Applications/Xcode_16.4.app/Contents/Developer
- name: List
run: |
xcodebuild -list
Expand All @@ -63,7 +63,7 @@ jobs:
uses: actions/checkout@main
- name: Select Xcode version
run: |
sudo xcode-select -switch /Applications/Xcode_16.3.app/Contents/Developer
sudo xcode-select -switch /Applications/Xcode_16.4.app/Contents/Developer
- name: Setup
run: gem install bundler; bundle install
- name: Build
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/swiftui-auth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
unit-tests:
name: Package Unit Tests
runs-on: macos-26
timeout-minutes: 15
timeout-minutes: 45
steps:
- uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938

Expand Down
48 changes: 23 additions & 25 deletions FirebaseStorageUI/Sources/FUIStorageImageLoader.m
Original file line number Diff line number Diff line change
Expand Up @@ -126,33 +126,31 @@ - (BOOL)canRequestImageForURL:(NSURL *)url {
}];
// Observe the progress changes
[download observeStatus:FIRStorageTaskStatusProgress handler:^(FIRStorageTaskSnapshot * _Nonnull snapshot) {
// Check progressive decoding if need
// Progressive decoding requires access to partial data via the internal GTMSessionFetcher,
// which was removed in Firebase Storage 9.0.0. Guard to prevent crash; progressive rendering
// is silently skipped on Firebase 9+.
if (options & SDWebImageProgressiveLoad) {
FIRStorageDownloadTask *task = (FIRStorageDownloadTask *)snapshot.task;
// Currently, FIRStorageDownloadTask does not have the API to grab partial data
// But since FirebaseUI and Firebase are seamless component, we access the internal fetcher here
GTMSessionFetcher *fetcher = task.fetcher;
// Get the partial image data
NSData *partialData = [fetcher.downloadedData copy];
// Get response
int64_t expectedSize = fetcher.response.expectedContentLength;
expectedSize = expectedSize > 0 ? expectedSize : 0;
int64_t receivedSize = fetcher.downloadedLength;
if (expectedSize != 0) {
// Get the finish status
BOOL finished = receivedSize >= expectedSize;
// This progress block may be called on main queue or global queue (depends configuration), always dispatched on coder queue
if (coderQueue.operationCount == 0) {
[coderQueue addOperationWithBlock:^{
UIImage *image = SDImageLoaderDecodeProgressiveImageData(partialData, url, finished, task, options, context);
if (image) {
dispatch_main_async_safe(^{
if (completedBlock) {
completedBlock(image, partialData, nil, NO);
}
});
}
}];
if ([task respondsToSelector:@selector(fetcher)]) {
GTMSessionFetcher *fetcher = [task performSelector:@selector(fetcher)];
NSData *partialData = [fetcher.downloadedData copy];
int64_t expectedSize = fetcher.response.expectedContentLength;
expectedSize = expectedSize > 0 ? expectedSize : 0;
int64_t receivedSize = fetcher.downloadedLength;
if (expectedSize != 0) {
BOOL finished = receivedSize >= expectedSize;
if (coderQueue.operationCount == 0) {
[coderQueue addOperationWithBlock:^{
UIImage *image = SDImageLoaderDecodeProgressiveImageData(partialData, url, finished, task, options, context);
if (image) {
dispatch_main_async_safe(^{
if (completedBlock) {
completedBlock(image, partialData, nil, NO);
}
});
}
}];
}
}
}
}
Expand Down
68 changes: 68 additions & 0 deletions FirebaseStorageUI/SwiftBridge/FirebaseStorageUISwift.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import UIKit
import SDWebImage

@_exported import FirebaseStorageUI
@_exported import FirebaseStorage

extension UIImageView {

public func sd_setImageWithStorageReference(
_ storageRef: StorageReference,
maxImageSize size: UInt64? = nil,
placeholderImage placeholder: UIImage? = nil,
options: SDWebImageOptions = [],
context: [SDWebImageContextOption: Any]? = nil,
completion: ((UIImage?, Error?, SDImageCacheType, StorageReference) -> Void)? = nil
) {
sd_setImageWithStorageReference(
storageRef,
maxImageSize: size,
placeholderImage: placeholder,
options: options,
context: context,
progress: nil,
completion: completion
)
}

public func sd_setImageWithStorageReference(
_ storageRef: StorageReference,
maxImageSize size: UInt64? = nil,
placeholderImage placeholder: UIImage? = nil,
options: SDWebImageOptions = [],
context: [SDWebImageContextOption: Any]? = nil,
progress progressBlock: ((Int, Int, StorageReference) -> Void)?,
completion: ((UIImage?, Error?, SDImageCacheType, StorageReference) -> Void)? = nil
) {
guard let url = Self.storageURL(for: storageRef) else { return }

var ctx = context ?? [:]
ctx[.imageLoader] = StorageImageLoader.shared
ctx[.fuiStorageMaxImageSize] = size ?? StorageImageLoader.shared.defaultMaxImageSize

let sdProgress: SDImageLoaderProgressBlock? = progressBlock.map { block in
{ received, expected, _ in block(Int(received), Int(expected), storageRef) }
}
let sdCompletion: SDExternalCompletionBlock? = completion.map { block in
{ image, error, cacheType, _ in block(image, error, cacheType, storageRef) }
}

sd_setImage(
with: url,
placeholderImage: placeholder,
options: options,
context: ctx,
progress: sdProgress,
completed: sdCompletion
)
}

private static func storageURL(for storageRef: StorageReference) -> URL? {
guard !storageRef.bucket.isEmpty else { return nil }
var components = URLComponents()
components.scheme = "gs"
components.host = storageRef.bucket
components.path = "/" + storageRef.fullPath
return components.url
}
Comment thread
demolaf marked this conversation as resolved.
}
13 changes: 13 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ let package = Package(
name: "FirebaseStorageUI",
targets: ["FirebaseStorageUI"]
),
.library(
name: "FirebaseStorageUISwift",
targets: ["FirebaseStorageUISwift"]
),
.library(
name: "FirebaseAuthSwiftUI",
targets: ["FirebaseAuthSwiftUI"]
Expand Down Expand Up @@ -126,6 +130,15 @@ let package = Package(
.headerSearchPath("../../"),
]
),
.target(
name: "FirebaseStorageUISwift",
dependencies: [
"FirebaseStorageUI",
.product(name: "FirebaseStorage", package: "firebase-ios-sdk"),
.product(name: "SDWebImage", package: "SDWebImage"),
],
path: "FirebaseStorageUI/SwiftBridge"
),
.target(
name: "FirebaseAuthUIComponents",
dependencies: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,16 @@ func createEmail() -> String {
}

@MainActor private func typeIntoField(_ field: XCUIElement,
text: String) throws {
field.tap()
field.typeText(text)
text: String,
app: XCUIApplication) throws {
UIPasteboard.general.string = text
let pasteMenuItem = try showPasteMenu(for: field, text: text, app: app)
pasteMenuItem.tap()

let success = waitForFieldValue(field, expectedText: text, timeout: 3)
UIPasteboard.general.string = nil

guard waitForFieldValue(field, expectedText: text) else {
guard success else {
throw NSError(
domain: "TestError",
code: 2,
Expand All @@ -110,10 +115,22 @@ func createEmail() -> String {
UIPasteboard.general.string = text
let pasteMenuItem = try showPasteMenu(for: field, text: text, app: app)
pasteMenuItem.tap()
usleep(200_000) // 0.2 seconds

// Poll until the value changes rather than relying on a fixed sleep.
// Secure fields show bullet characters so we can only detect a change, not the exact value.
let deadline = Date().addingTimeInterval(3.0)
var pasted = false
while Date() < deadline {
if (field.value as? String) != originalValue {
pasted = true
break
}
RunLoop.current.run(until: Date().addingTimeInterval(0.1))
}

UIPasteboard.general.string = nil

guard (field.value as? String) != originalValue else {
guard pasted else {
throw NSError(
domain: "TestError",
code: 3,
Expand All @@ -134,7 +151,7 @@ func createEmail() -> String {
case .secureTextField:
try pasteIntoSecureField(field, text: text, app: app)
default:
try typeIntoField(field, text: text)
try typeIntoField(field, text: text, app: app)
}
}

Expand Down
Loading
Loading