[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:
Byeong Gwan 2025-02-26 17:26:10 +09:00 committed by GitHub
parent 5d23a7df19
commit caf7700e2c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 95 additions and 55 deletions

View File

@ -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

View File

@ -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
} }

View File

@ -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(

View File

@ -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
}
} }