mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-03-05 18:26:52 +08:00
[iOS] Refactor error and data access (#2733)
* implement safe bounded data Access and cancellation supporting URLSession downloadTask * declare specific SpineError type declaring conformance to Error on String is discouraged, and Creating own Error type is recommended * use explicit Error initializing rather than casing syntax (apply requested change)
This commit is contained in:
parent
5d23a7df19
commit
caf7700e2c
@ -59,7 +59,7 @@ public final class SkinAndAnimationBounds: NSObject, BoundsProvider {
|
|||||||
/// the bounding box of the skeleton. If no skins are given, the default skin is used.
|
/// the bounding box of the skeleton. If no skins are given, the default skin is used.
|
||||||
/// The `stepTime`, given in seconds, defines at what interval the bounds should be sampled
|
/// The `stepTime`, given in seconds, defines at what interval the bounds should be sampled
|
||||||
/// across the entire animation.
|
/// across the entire animation.
|
||||||
public init(animation: String? = nil, skins: [String]? = nil, let stepTime: TimeInterval = 0.1) {
|
public init(animation: String? = nil, skins: [String]? = nil, stepTime: TimeInterval = 0.1) {
|
||||||
self.animation = animation
|
self.animation = animation
|
||||||
if let skins, !skins.isEmpty {
|
if let skins, !skins.isEmpty {
|
||||||
self.skins = skins
|
self.skins = skins
|
||||||
|
|||||||
@ -27,7 +27,7 @@ public extension SkeletonDrawableWrapper {
|
|||||||
spineView.delegate?.draw(in: spineView)
|
spineView.delegate?.draw(in: spineView)
|
||||||
|
|
||||||
guard let texture = spineView.currentDrawable?.texture else {
|
guard let texture = spineView.currentDrawable?.texture else {
|
||||||
throw "Could not read texture."
|
throw SpineError("Could not read texture.")
|
||||||
}
|
}
|
||||||
let width = texture.width
|
let width = texture.width
|
||||||
let height = texture.height
|
let height = texture.height
|
||||||
@ -47,7 +47,7 @@ public extension SkeletonDrawableWrapper {
|
|||||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||||
guard let context = CGContext(data: data, width: width, height: height, bitsPerComponent: 8, bytesPerRow: rowBytes, space: colorSpace, bitmapInfo: bitmapInfo.rawValue),
|
guard let context = CGContext(data: data, width: width, height: height, bitsPerComponent: 8, bytesPerRow: rowBytes, space: colorSpace, bitmapInfo: bitmapInfo.rawValue),
|
||||||
let cgImage = context.makeImage() else {
|
let cgImage = context.makeImage() else {
|
||||||
throw "Could not create image."
|
throw SpineError("Could not create image.")
|
||||||
}
|
}
|
||||||
return cgImage
|
return cgImage
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,22 +94,22 @@ public final class SkeletonDrawableWrapper: NSObject {
|
|||||||
self.skeletonData = skeletonData
|
self.skeletonData = skeletonData
|
||||||
|
|
||||||
guard let nativeSkeletonDrawable = spine_skeleton_drawable_create(skeletonData.wrappee) else {
|
guard let nativeSkeletonDrawable = spine_skeleton_drawable_create(skeletonData.wrappee) else {
|
||||||
throw "Could not load native skeleton drawable"
|
throw SpineError("Could not load native skeleton drawable")
|
||||||
}
|
}
|
||||||
skeletonDrawable = SkeletonDrawable(nativeSkeletonDrawable)
|
skeletonDrawable = SkeletonDrawable(nativeSkeletonDrawable)
|
||||||
|
|
||||||
guard let nativeSkeleton = spine_skeleton_drawable_get_skeleton(skeletonDrawable.wrappee) else {
|
guard let nativeSkeleton = spine_skeleton_drawable_get_skeleton(skeletonDrawable.wrappee) else {
|
||||||
throw "Could not load native skeleton"
|
throw SpineError("Could not load native skeleton")
|
||||||
}
|
}
|
||||||
skeleton = Skeleton(nativeSkeleton)
|
skeleton = Skeleton(nativeSkeleton)
|
||||||
|
|
||||||
guard let nativeAnimationStateData = spine_skeleton_drawable_get_animation_state_data(skeletonDrawable.wrappee) else {
|
guard let nativeAnimationStateData = spine_skeleton_drawable_get_animation_state_data(skeletonDrawable.wrappee) else {
|
||||||
throw "Could not load native animation state data"
|
throw SpineError("Could not load native animation state data")
|
||||||
}
|
}
|
||||||
animationStateData = AnimationStateData(nativeAnimationStateData)
|
animationStateData = AnimationStateData(nativeAnimationStateData)
|
||||||
|
|
||||||
guard let nativeAnimationState = spine_skeleton_drawable_get_animation_state(skeletonDrawable.wrappee) else {
|
guard let nativeAnimationState = spine_skeleton_drawable_get_animation_state(skeletonDrawable.wrappee) else {
|
||||||
throw "Could not load native animation state"
|
throw SpineError("Could not load native animation state")
|
||||||
}
|
}
|
||||||
animationState = AnimationState(nativeAnimationState)
|
animationState = AnimationState(nativeAnimationState)
|
||||||
animationStateWrapper = AnimationStateWrapper(
|
animationStateWrapper = AnimationStateWrapper(
|
||||||
|
|||||||
@ -57,18 +57,19 @@ public extension Atlas {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static func fromData(data: Data, loadFile: (_ name: String) async throws -> Data) async throws -> (Atlas, [UIImage]) {
|
private static func fromData(data: Data, loadFile: (_ name: String) async throws -> Data) async throws -> (Atlas, [UIImage]) {
|
||||||
guard let atlasData = String(data: data, encoding: .utf8) as? NSString else {
|
guard let atlasData = String(data: data, encoding: .utf8) else {
|
||||||
throw "Couldn't read atlas bytes as utf8 string"
|
throw SpineError("Couldn't read atlas bytes as utf8 string")
|
||||||
}
|
}
|
||||||
let atlasDataNative = UnsafeMutablePointer<CChar>(mutating: atlasData.utf8String)
|
let atlas = try atlasData.utf8CString.withUnsafeBufferPointer {
|
||||||
guard let atlas = spine_atlas_load(atlasDataNative) else {
|
guard let atlas = spine_atlas_load($0.baseAddress) else {
|
||||||
throw "Couldn't load atlas data"
|
throw SpineError("Couldn't load atlas data")
|
||||||
|
}
|
||||||
|
return atlas
|
||||||
}
|
}
|
||||||
|
|
||||||
if let error = spine_atlas_get_error(atlas) {
|
if let error = spine_atlas_get_error(atlas) {
|
||||||
let message = String(cString: error)
|
let message = String(cString: error)
|
||||||
spine_atlas_dispose(atlas)
|
spine_atlas_dispose(atlas)
|
||||||
throw "Couldn't load atlas: \(message)"
|
throw SpineError("Couldn't load atlas: \(message)")
|
||||||
}
|
}
|
||||||
|
|
||||||
var atlasPages = [UIImage]()
|
var atlasPages = [UIImage]()
|
||||||
@ -130,26 +131,31 @@ public extension SkeletonData {
|
|||||||
///
|
///
|
||||||
/// Throws an `Error` in case the skeleton data could not be loaded.
|
/// Throws an `Error` in case the skeleton data could not be loaded.
|
||||||
static func fromData(atlas: Atlas, data: Data) throws -> SkeletonData {
|
static func fromData(atlas: Atlas, data: Data) throws -> SkeletonData {
|
||||||
let binaryNative = try data.withUnsafeBytes { unsafeBytes in
|
let result = try data.withUnsafeBytes{
|
||||||
guard let bytes = unsafeBytes.bindMemory(to: UInt8.self).baseAddress else {
|
try $0.withMemoryRebound(to: UInt8.self) { buffer in
|
||||||
throw "Couldn't read atlas binary"
|
guard let ptr = buffer.baseAddress else {
|
||||||
|
throw SpineError("Couldn't read atlas binary")
|
||||||
|
}
|
||||||
|
return spine_skeleton_data_load_binary(
|
||||||
|
atlas.wrappee,
|
||||||
|
ptr,
|
||||||
|
Int32(buffer.count)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return (data: bytes, length: Int32(unsafeBytes.count))
|
|
||||||
}
|
}
|
||||||
let result = spine_skeleton_data_load_binary(
|
guard let result else {
|
||||||
atlas.wrappee,
|
throw SpineError("Couldn't load skeleton data")
|
||||||
binaryNative.data,
|
}
|
||||||
binaryNative.length
|
defer {
|
||||||
)
|
spine_skeleton_data_result_dispose(result)
|
||||||
|
}
|
||||||
if let error = spine_skeleton_data_result_get_error(result) {
|
if let error = spine_skeleton_data_result_get_error(result) {
|
||||||
let message = String(cString: error)
|
let message = String(cString: error)
|
||||||
spine_skeleton_data_result_dispose(result)
|
throw SpineError("Couldn't load skeleton data: \(message)")
|
||||||
throw "Couldn't load skeleton data: \(message)"
|
|
||||||
}
|
}
|
||||||
guard let data = spine_skeleton_data_result_get_data(result) else {
|
guard let data = spine_skeleton_data_result_get_data(result) else {
|
||||||
throw "Couldn't load skeleton data from result"
|
throw SpineError("Couldn't load skeleton data from result")
|
||||||
}
|
}
|
||||||
spine_skeleton_data_result_dispose(result)
|
|
||||||
return SkeletonData(data)
|
return SkeletonData(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,26 +164,31 @@ public extension SkeletonData {
|
|||||||
///
|
///
|
||||||
/// Throws an `Error` in case the atlas could not be loaded.
|
/// Throws an `Error` in case the atlas could not be loaded.
|
||||||
static func fromJson(atlas: Atlas, json: String) throws -> SkeletonData {
|
static func fromJson(atlas: Atlas, json: String) throws -> SkeletonData {
|
||||||
let jsonNative = UnsafeMutablePointer<CChar>(mutating: (json as NSString).utf8String)
|
let result = try json.utf8CString.withUnsafeBufferPointer { buffer in
|
||||||
guard let result = spine_skeleton_data_load_json(atlas.wrappee, jsonNative) else {
|
guard
|
||||||
throw "Couldn't load skeleton data json"
|
let basePtr = buffer.baseAddress,
|
||||||
|
let result = spine_skeleton_data_load_json(atlas.wrappee, basePtr) else {
|
||||||
|
throw SpineError("Couldn't load skeleton data json")
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
defer {
|
||||||
|
spine_skeleton_data_result_dispose(result)
|
||||||
}
|
}
|
||||||
if let error = spine_skeleton_data_result_get_error(result) {
|
if let error = spine_skeleton_data_result_get_error(result) {
|
||||||
let message = String(cString: error)
|
let message = String(cString: error)
|
||||||
spine_skeleton_data_result_dispose(result)
|
throw SpineError("Couldn't load skeleton data: \(message)")
|
||||||
throw "Couldn't load skeleton data: \(message)"
|
|
||||||
}
|
}
|
||||||
guard let data = spine_skeleton_data_result_get_data(result) else {
|
guard let data = spine_skeleton_data_result_get_data(result) else {
|
||||||
throw "Couldn't load skeleton data from result"
|
throw SpineError("Couldn't load skeleton data from result")
|
||||||
}
|
}
|
||||||
spine_skeleton_data_result_dispose(result)
|
|
||||||
return SkeletonData(data)
|
return SkeletonData(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func fromData(atlas: Atlas, data: Data, isJson: Bool) throws -> SkeletonData {
|
private static func fromData(atlas: Atlas, data: Data, isJson: Bool) throws -> SkeletonData {
|
||||||
if isJson {
|
if isJson {
|
||||||
guard let json = String(data: data, encoding: .utf8) else {
|
guard let json = String(data: data, encoding: .utf8) else {
|
||||||
throw "Couldn't read skeleton data json string"
|
throw SpineError("Couldn't read skeleton data json string")
|
||||||
}
|
}
|
||||||
return try fromJson(atlas: atlas, json: json)
|
return try fromJson(atlas: atlas, json: json)
|
||||||
} else {
|
} else {
|
||||||
@ -271,12 +282,12 @@ internal enum FileSource {
|
|||||||
case .bundle(let fileName, let bundle):
|
case .bundle(let fileName, let bundle):
|
||||||
let components = fileName.split(separator: ".")
|
let components = fileName.split(separator: ".")
|
||||||
guard components.count > 1, let ext = components.last else {
|
guard components.count > 1, let ext = components.last else {
|
||||||
throw "Provide both file name and file extension"
|
throw SpineError("Provide both file name and file extension")
|
||||||
}
|
}
|
||||||
let name = components.dropLast(1).joined(separator: ".")
|
let name = components.dropLast(1).joined(separator: ".")
|
||||||
|
|
||||||
guard let fileUrl = bundle.url(forResource: name, withExtension: String(ext)) else {
|
guard let fileUrl = bundle.url(forResource: name, withExtension: String(ext)) else {
|
||||||
throw "Could not load file with name \(name) from bundle"
|
throw SpineError("Could not load file with name \(name) from bundle")
|
||||||
}
|
}
|
||||||
return try Data(contentsOf: fileUrl, options: [])
|
return try Data(contentsOf: fileUrl, options: [])
|
||||||
case .file(let fileUrl):
|
case .file(let fileUrl):
|
||||||
@ -289,34 +300,63 @@ internal enum FileSource {
|
|||||||
}
|
}
|
||||||
return try Data(contentsOf: temp, options: [])
|
return try Data(contentsOf: temp, options: [])
|
||||||
} else {
|
} else {
|
||||||
return try await withCheckedThrowingContinuation { continuation in
|
let lock = NSRecursiveLock()
|
||||||
let task = URLSession.shared.downloadTask(with: url) { temp, response, error in
|
nonisolated(unsafe)
|
||||||
if let error {
|
var isCancelled = false
|
||||||
continuation.resume(throwing: error)
|
nonisolated(unsafe)
|
||||||
} else {
|
var taskHolder:URLSessionDownloadTask? = nil
|
||||||
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
|
return try await withTaskCancellationHandler {
|
||||||
continuation.resume(throwing: URLError(.badServerResponse))
|
try await withCheckedThrowingContinuation { continuation in
|
||||||
return
|
let task = URLSession.shared.downloadTask(with: url) { temp, response, error in
|
||||||
}
|
if let error {
|
||||||
guard let temp else {
|
|
||||||
continuation.resume(throwing: "Could not download file.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
do {
|
|
||||||
continuation.resume(returning: try Data(contentsOf: temp, options: []))
|
|
||||||
} catch {
|
|
||||||
continuation.resume(throwing: error)
|
continuation.resume(throwing: error)
|
||||||
|
} else {
|
||||||
|
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
|
||||||
|
continuation.resume(throwing: URLError(.badServerResponse))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let temp else {
|
||||||
|
continuation.resume(throwing: SpineError("Could not download file."))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
continuation.resume(returning: try Data(contentsOf: temp, options: []))
|
||||||
|
} catch {
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
task.resume()
|
||||||
|
let shouldCancel = lock.withLock {
|
||||||
|
if !isCancelled {
|
||||||
|
taskHolder = task
|
||||||
|
}
|
||||||
|
return isCancelled
|
||||||
|
}
|
||||||
|
if shouldCancel {
|
||||||
|
task.cancel()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
task.resume()
|
} onCancel: {
|
||||||
|
lock.withLock {
|
||||||
|
isCancelled = true
|
||||||
|
let value = taskHolder
|
||||||
|
taskHolder = nil
|
||||||
|
return value
|
||||||
|
}?.cancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension String: Error {
|
public struct SpineError: Error, CustomStringConvertible {
|
||||||
|
|
||||||
|
public let description: String
|
||||||
|
|
||||||
|
internal init(_ description: String) {
|
||||||
|
self.description = description
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user