导入 XuXuCheck 工程(iOS 干化学尿液试纸分析 App)
从桌面工程复制而来,作为本仓库的 Xuxucheck/ 子目录。 为控制仓库体积做了两处删减: - 移除 Frameworks/opencv2.xcframework 下的两个 visionOS 切片 (xros-arm64、xros-arm64-simulator,合计约 106 MB),并同步删掉该 xcframework 内 Info.plist 里对应的两条 AvailableLibraries 记录, 只保留 ios-arm64 与 ios-arm64_x86_64-simulator。 - 移除全部 .DS_Store 与 xcuserdata/。 已用 xcodebuild 从本目录完整编译验证:BUILD SUCCEEDED, ProcessXCFramework 以 --platform ios 成功选中框架,产物含 3699 个 cv:: 符号 (OpenCV 确实链入),嵌入 framework 3 个,与移除无用 pod 之后的状态一致。 仓库约 206 MB,其中 OpenCV 203 MB、其余源码与依赖约 3 MB。
This commit is contained in:
@@ -0,0 +1,406 @@
|
||||
//
|
||||
// Created by Lorenzo Fiamingo on 04/11/20.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
/// A view that asynchronously loads, cache and displays an image.
|
||||
///
|
||||
/// This view uses a custom default
|
||||
/// <doc://com.apple.documentation/documentation/Foundation/URLSession>
|
||||
/// instance to load an image from the specified URL, and then display it.
|
||||
/// For example, you can display an icon that's stored on a server:
|
||||
///
|
||||
/// CachedAsyncImage(url: URL(string: "https://example.com/icon.png"))
|
||||
/// .frame(width: 200, height: 200)
|
||||
///
|
||||
/// Until the image loads, the view displays a standard placeholder that
|
||||
/// fills the available space. After the load completes successfully, the view
|
||||
/// updates to display the image. In the example above, the icon is smaller
|
||||
/// than the frame, and so appears smaller than the placeholder.
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// You can specify a custom placeholder using
|
||||
/// ``init(url:urlCache:scale:content:placeholder:)``. With this initializer, you can
|
||||
/// also use the `content` parameter to manipulate the loaded image.
|
||||
/// For example, you can add a modifier to make the loaded image resizable:
|
||||
///
|
||||
/// CachedAsyncImage(url: URL(string: "https://example.com/icon.png")) { image in
|
||||
/// image.resizable()
|
||||
/// } placeholder: {
|
||||
/// ProgressView()
|
||||
/// }
|
||||
/// .frame(width: 50, height: 50)
|
||||
///
|
||||
/// For this example, SwiftUI shows a ``ProgressView`` first, and then the
|
||||
/// image scaled to fit in the specified frame:
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// > Important: You can't apply image-specific modifiers, like
|
||||
/// ``Image/resizable(capInsets:resizingMode:)``, directly to a `CachedAsyncImage`.
|
||||
/// Instead, apply them to the ``Image`` instance that your `content`
|
||||
/// closure gets when defining the view's appearance.
|
||||
///
|
||||
/// To gain more control over the loading process, use the
|
||||
/// ``init(url:urlCache:scale:transaction:content:)`` initializer, which takes a
|
||||
/// `content` closure that receives an ``AsyncImagePhase`` to indicate
|
||||
/// the state of the loading operation. Return a view that's appropriate
|
||||
/// for the current phase:
|
||||
///
|
||||
/// CachedAsyncImage(url: URL(string: "https://example.com/icon.png")) { phase in
|
||||
/// if let image = phase.image {
|
||||
/// image // Displays the loaded image.
|
||||
/// } else if phase.error != nil {
|
||||
/// Color.red // Indicates an error.
|
||||
/// } else {
|
||||
/// Color.blue // Acts as a placeholder.
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
|
||||
public struct CachedAsyncImage<Content>: View where Content: View {
|
||||
|
||||
@State private var phase: AsyncImagePhase
|
||||
|
||||
private let urlRequest: URLRequest?
|
||||
|
||||
private let urlSession: URLSession
|
||||
|
||||
private let scale: CGFloat
|
||||
|
||||
private let transaction: Transaction
|
||||
|
||||
private let content: (AsyncImagePhase) -> Content
|
||||
|
||||
public var body: some View {
|
||||
content(phase)
|
||||
.task(id: urlRequest, load)
|
||||
}
|
||||
|
||||
/// Loads and displays an image from the specified URL.
|
||||
///
|
||||
/// Until the image loads, SwiftUI displays a default placeholder. When
|
||||
/// the load operation completes successfully, SwiftUI updates the
|
||||
/// view to show the loaded image. If the operation fails, SwiftUI
|
||||
/// continues to display the placeholder. The following example loads
|
||||
/// and displays an icon from an example server:
|
||||
///
|
||||
/// CachedAsyncImage(url: URL(string: "https://example.com/icon.png"))
|
||||
///
|
||||
/// If you want to customize the placeholder or apply image-specific
|
||||
/// modifiers --- like ``Image/resizable(capInsets:resizingMode:)`` ---
|
||||
/// to the loaded image, use the ``init(url:scale:content:placeholder:)``
|
||||
/// initializer instead.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: The URL of the image to display.
|
||||
/// - urlCache: The URL cache for providing cached responses to requests within the session.
|
||||
/// - scale: The scale to use for the image. The default is `1`. Set a
|
||||
/// different value when loading images designed for higher resolution
|
||||
/// displays. For example, set a value of `2` for an image that you
|
||||
/// would name with the `@2x` suffix if stored in a file on disk.
|
||||
public init(url: URL?, urlCache: URLCache = .shared, scale: CGFloat = 1) where Content == Image {
|
||||
let urlRequest = url == nil ? nil : URLRequest(url: url!)
|
||||
self.init(urlRequest: urlRequest, urlCache: urlCache, scale: scale)
|
||||
}
|
||||
|
||||
/// Loads and displays an image from the specified URL.
|
||||
///
|
||||
/// Until the image loads, SwiftUI displays a default placeholder. When
|
||||
/// the load operation completes successfully, SwiftUI updates the
|
||||
/// view to show the loaded image. If the operation fails, SwiftUI
|
||||
/// continues to display the placeholder. The following example loads
|
||||
/// and displays an icon from an example server:
|
||||
///
|
||||
/// CachedAsyncImage(url: URL(string: "https://example.com/icon.png"))
|
||||
///
|
||||
/// If you want to customize the placeholder or apply image-specific
|
||||
/// modifiers --- like ``Image/resizable(capInsets:resizingMode:)`` ---
|
||||
/// to the loaded image, use the ``init(url:scale:content:placeholder:)``
|
||||
/// initializer instead.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - urlRequest: The URL request of the image to display.
|
||||
/// - urlCache: The URL cache for providing cached responses to requests within the session.
|
||||
/// - scale: The scale to use for the image. The default is `1`. Set a
|
||||
/// different value when loading images designed for higher resolution
|
||||
/// displays. For example, set a value of `2` for an image that you
|
||||
/// would name with the `@2x` suffix if stored in a file on disk.
|
||||
public init(urlRequest: URLRequest?, urlCache: URLCache = .shared, scale: CGFloat = 1) where Content == Image {
|
||||
self.init(urlRequest: urlRequest, urlCache: urlCache, scale: scale) { phase in
|
||||
#if os(macOS)
|
||||
phase.image ?? Image(nsImage: .init())
|
||||
#else
|
||||
phase.image ?? Image(uiImage: .init())
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads and displays a modifiable image from the specified URL using
|
||||
/// a custom placeholder until the image loads.
|
||||
///
|
||||
/// Until the image loads, SwiftUI displays the placeholder view that
|
||||
/// you specify. When the load operation completes successfully, SwiftUI
|
||||
/// updates the view to show content that you specify, which you
|
||||
/// create using the loaded image. For example, you can show a green
|
||||
/// placeholder, followed by a tiled version of the loaded image:
|
||||
///
|
||||
/// CachedAsyncImage(url: URL(string: "https://example.com/icon.png")) { image in
|
||||
/// image.resizable(resizingMode: .tile)
|
||||
/// } placeholder: {
|
||||
/// Color.green
|
||||
/// }
|
||||
///
|
||||
/// If the load operation fails, SwiftUI continues to display the
|
||||
/// placeholder. To be able to display a different view on a load error,
|
||||
/// use the ``init(url:scale:transaction:content:)`` initializer instead.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: The URL of the image to display.
|
||||
/// - urlCache: The URL cache for providing cached responses to requests within the session.
|
||||
/// - scale: The scale to use for the image. The default is `1`. Set a
|
||||
/// different value when loading images designed for higher resolution
|
||||
/// displays. For example, set a value of `2` for an image that you
|
||||
/// would name with the `@2x` suffix if stored in a file on disk.
|
||||
/// - content: A closure that takes the loaded image as an input, and
|
||||
/// returns the view to show. You can return the image directly, or
|
||||
/// modify it as needed before returning it.
|
||||
/// - placeholder: A closure that returns the view to show until the
|
||||
/// load operation completes successfully.
|
||||
public init<I, P>(url: URL?, urlCache: URLCache = .shared, scale: CGFloat = 1, @ViewBuilder content: @escaping (Image) -> I, @ViewBuilder placeholder: @escaping () -> P) where Content == _ConditionalContent<I, P>, I : View, P : View {
|
||||
let urlRequest = url == nil ? nil : URLRequest(url: url!)
|
||||
self.init(urlRequest: urlRequest, urlCache: urlCache, scale: scale, content: content, placeholder: placeholder)
|
||||
}
|
||||
|
||||
/// Loads and displays a modifiable image from the specified URL using
|
||||
/// a custom placeholder until the image loads.
|
||||
///
|
||||
/// Until the image loads, SwiftUI displays the placeholder view that
|
||||
/// you specify. When the load operation completes successfully, SwiftUI
|
||||
/// updates the view to show content that you specify, which you
|
||||
/// create using the loaded image. For example, you can show a green
|
||||
/// placeholder, followed by a tiled version of the loaded image:
|
||||
///
|
||||
/// CachedAsyncImage(url: URL(string: "https://example.com/icon.png")) { image in
|
||||
/// image.resizable(resizingMode: .tile)
|
||||
/// } placeholder: {
|
||||
/// Color.green
|
||||
/// }
|
||||
///
|
||||
/// If the load operation fails, SwiftUI continues to display the
|
||||
/// placeholder. To be able to display a different view on a load error,
|
||||
/// use the ``init(url:scale:transaction:content:)`` initializer instead.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - urlRequest: The URL request of the image to display.
|
||||
/// - urlCache: The URL cache for providing cached responses to requests within the session.
|
||||
/// - scale: The scale to use for the image. The default is `1`. Set a
|
||||
/// different value when loading images designed for higher resolution
|
||||
/// displays. For example, set a value of `2` for an image that you
|
||||
/// would name with the `@2x` suffix if stored in a file on disk.
|
||||
/// - content: A closure that takes the loaded image as an input, and
|
||||
/// returns the view to show. You can return the image directly, or
|
||||
/// modify it as needed before returning it.
|
||||
/// - placeholder: A closure that returns the view to show until the
|
||||
/// load operation completes successfully.
|
||||
public init<I, P>(urlRequest: URLRequest?, urlCache: URLCache = .shared, scale: CGFloat = 1, @ViewBuilder content: @escaping (Image) -> I, @ViewBuilder placeholder: @escaping () -> P) where Content == _ConditionalContent<I, P>, I : View, P : View {
|
||||
self.init(urlRequest: urlRequest, urlCache: urlCache, scale: scale) { phase in
|
||||
if let image = phase.image {
|
||||
content(image)
|
||||
} else {
|
||||
placeholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads and displays a modifiable image from the specified URL in phases.
|
||||
///
|
||||
/// If you set the asynchronous image's URL to `nil`, or after you set the
|
||||
/// URL to a value but before the load operation completes, the phase is
|
||||
/// ``AsyncImagePhase/empty``. After the operation completes, the phase
|
||||
/// becomes either ``AsyncImagePhase/failure(_:)`` or
|
||||
/// ``AsyncImagePhase/success(_:)``. In the first case, the phase's
|
||||
/// ``AsyncImagePhase/error`` value indicates the reason for failure.
|
||||
/// In the second case, the phase's ``AsyncImagePhase/image`` property
|
||||
/// contains the loaded image. Use the phase to drive the output of the
|
||||
/// `content` closure, which defines the view's appearance:
|
||||
///
|
||||
/// CachedAsyncImage(url: URL(string: "https://example.com/icon.png")) { phase in
|
||||
/// if let image = phase.image {
|
||||
/// image // Displays the loaded image.
|
||||
/// } else if phase.error != nil {
|
||||
/// Color.red // Indicates an error.
|
||||
/// } else {
|
||||
/// Color.blue // Acts as a placeholder.
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// To add transitions when you change the URL, apply an identifier to the
|
||||
/// ``CachedAsyncImage``.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: The URL of the image to display.
|
||||
/// - urlCache: The URL cache for providing cached responses to requests within the session.
|
||||
/// - scale: The scale to use for the image. The default is `1`. Set a
|
||||
/// different value when loading images designed for higher resolution
|
||||
/// displays. For example, set a value of `2` for an image that you
|
||||
/// would name with the `@2x` suffix if stored in a file on disk.
|
||||
/// - transaction: The transaction to use when the phase changes.
|
||||
/// - content: A closure that takes the load phase as an input, and
|
||||
/// returns the view to display for the specified phase.
|
||||
public init(url: URL?, urlCache: URLCache = .shared, scale: CGFloat = 1, transaction: Transaction = Transaction(), @ViewBuilder content: @escaping (AsyncImagePhase) -> Content) {
|
||||
let urlRequest = url == nil ? nil : URLRequest(url: url!)
|
||||
self.init(urlRequest: urlRequest, urlCache: urlCache, scale: scale, transaction: transaction, content: content)
|
||||
}
|
||||
|
||||
/// Loads and displays a modifiable image from the specified URL in phases.
|
||||
///
|
||||
/// If you set the asynchronous image's URL to `nil`, or after you set the
|
||||
/// URL to a value but before the load operation completes, the phase is
|
||||
/// ``AsyncImagePhase/empty``. After the operation completes, the phase
|
||||
/// becomes either ``AsyncImagePhase/failure(_:)`` or
|
||||
/// ``AsyncImagePhase/success(_:)``. In the first case, the phase's
|
||||
/// ``AsyncImagePhase/error`` value indicates the reason for failure.
|
||||
/// In the second case, the phase's ``AsyncImagePhase/image`` property
|
||||
/// contains the loaded image. Use the phase to drive the output of the
|
||||
/// `content` closure, which defines the view's appearance:
|
||||
///
|
||||
/// CachedAsyncImage(url: URL(string: "https://example.com/icon.png")) { phase in
|
||||
/// if let image = phase.image {
|
||||
/// image // Displays the loaded image.
|
||||
/// } else if phase.error != nil {
|
||||
/// Color.red // Indicates an error.
|
||||
/// } else {
|
||||
/// Color.blue // Acts as a placeholder.
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// To add transitions when you change the URL, apply an identifier to the
|
||||
/// ``CachedAsyncImage``.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - urlRequest: The URL request of the image to display.
|
||||
/// - urlCache: The URL cache for providing cached responses to requests within the session.
|
||||
/// - scale: The scale to use for the image. The default is `1`. Set a
|
||||
/// different value when loading images designed for higher resolution
|
||||
/// displays. For example, set a value of `2` for an image that you
|
||||
/// would name with the `@2x` suffix if stored in a file on disk.
|
||||
/// - transaction: The transaction to use when the phase changes.
|
||||
/// - content: A closure that takes the load phase as an input, and
|
||||
/// returns the view to display for the specified phase.
|
||||
public init(urlRequest: URLRequest?, urlCache: URLCache = .shared, scale: CGFloat = 1, transaction: Transaction = Transaction(), @ViewBuilder content: @escaping (AsyncImagePhase) -> Content) {
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.urlCache = urlCache
|
||||
self.urlRequest = urlRequest
|
||||
self.urlSession = URLSession(configuration: configuration)
|
||||
self.scale = scale
|
||||
self.transaction = transaction
|
||||
self.content = content
|
||||
|
||||
self._phase = State(wrappedValue: .empty)
|
||||
do {
|
||||
if let urlRequest = urlRequest, let image = try cachedImage(from: urlRequest, cache: urlCache) {
|
||||
self._phase = State(wrappedValue: .success(image))
|
||||
}
|
||||
} catch {
|
||||
self._phase = State(wrappedValue: .failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
@Sendable
|
||||
private func load() async {
|
||||
do {
|
||||
if let urlRequest = urlRequest {
|
||||
let (image, metrics) = try await remoteImage(from: urlRequest, session: urlSession)
|
||||
if metrics.transactionMetrics.last?.resourceFetchType == .localCache {
|
||||
// WARNING: This does not behave well when the url is changed with another
|
||||
phase = .success(image)
|
||||
} else {
|
||||
withAnimation(transaction.animation) {
|
||||
phase = .success(image)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
withAnimation(transaction.animation) {
|
||||
phase = .empty
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
withAnimation(transaction.animation) {
|
||||
phase = .failure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - LoadingError
|
||||
|
||||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
|
||||
private extension AsyncImage {
|
||||
|
||||
struct LoadingError: Error {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
|
||||
private extension CachedAsyncImage {
|
||||
private func remoteImage(from request: URLRequest, session: URLSession) async throws -> (Image, URLSessionTaskMetrics) {
|
||||
let (data, _, metrics) = try await session.data(for: request)
|
||||
if metrics.redirectCount > 0, let lastResponse = metrics.transactionMetrics.last?.response {
|
||||
let requests = metrics.transactionMetrics.map { $0.request }
|
||||
requests.forEach(session.configuration.urlCache!.removeCachedResponse)
|
||||
let lastCachedResponse = CachedURLResponse(response: lastResponse, data: data)
|
||||
session.configuration.urlCache!.storeCachedResponse(lastCachedResponse, for: request)
|
||||
}
|
||||
return (try image(from: data), metrics)
|
||||
}
|
||||
|
||||
private func cachedImage(from request: URLRequest, cache: URLCache) throws -> Image? {
|
||||
guard let cachedResponse = cache.cachedResponse(for: request) else { return nil }
|
||||
return try image(from: cachedResponse.data)
|
||||
}
|
||||
|
||||
private func image(from data: Data) throws -> Image {
|
||||
#if os(macOS)
|
||||
if let nsImage = NSImage(data: data) {
|
||||
return Image(nsImage: nsImage)
|
||||
} else {
|
||||
throw AsyncImage<Content>.LoadingError()
|
||||
}
|
||||
#else
|
||||
if let uiImage = UIImage(data: data, scale: scale) {
|
||||
return Image(uiImage: uiImage)
|
||||
} else {
|
||||
throw AsyncImage<Content>.LoadingError()
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AsyncImageURLSession
|
||||
|
||||
private class URLSessionTaskController: NSObject, URLSessionTaskDelegate {
|
||||
|
||||
var metrics: URLSessionTaskMetrics?
|
||||
|
||||
func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
|
||||
self.metrics = metrics
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *)
|
||||
private extension URLSession {
|
||||
|
||||
func data(for request: URLRequest) async throws -> (Data, URLResponse, URLSessionTaskMetrics) {
|
||||
let controller = URLSessionTaskController()
|
||||
let (data, response) = try await data(for: request, delegate: controller)
|
||||
return (data, response, controller.metrics!)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user