import Foundation import SwiftUI import SpineCppLite public var version: String { return "\(majorVersion).\(minorVersion)" } public var majorVersion: Int { return Int(spine_major_version()) } public var minorVersion: Int { return Int(spine_minor_version()) } /// ``Atlas`` data loaded from a `.atlas` file and its corresponding `.png` files. For each atlas image, /// a corresponding `UIImage` is constructed, which is used when rendering a skeleton /// that uses this atlas. /// /// Use the static methods ``Atlas/fromBundle(_:bundle:)``, ``Atlas/fromFile(_:)``, and ``Atlas/fromHttp(_:)`` to load an atlas. Call ``Atlas/dispose()` /// when the atlas is no longer in use to release its resources. public extension Atlas { /// Loads an ``Atlas`` from the file with name `atlasFileName` in the `main` bundle or the optionally provided [bundle]. /// /// Throws an `Error` in case the atlas could not be loaded. static func fromBundle(_ atlasFileName: String, bundle: Bundle = .main) async throws -> (Atlas, [UIImage]) { let data = try await FileSource.bundle(fileName: atlasFileName, bundle: bundle).load() return try await Self.fromData(data: data) { name in return try await FileSource.bundle(fileName: name, bundle: bundle).load() } } /// Loads an ``Atlas`` from the file URL `atlasFile`. /// /// Throws an `Error` in case the atlas could not be loaded. static func fromFile(_ atlasFile: URL) async throws -> (Atlas, [UIImage]) { let data = try await FileSource.file(atlasFile).load() return try await Self.fromData(data: data) { name in let dir = atlasFile.deletingLastPathComponent() let file = dir.appendingPathComponent(name) return try await FileSource.file(file).load() } } /// Loads an ``Atlas`` from the http URL `atlasURL`. /// /// Throws an `Error` in case the atlas could not be loaded. static func fromHttp(_ atlasURL: URL) async throws -> (Atlas, [UIImage]) { let data = try await FileSource.http(atlasURL).load() return try await Self.fromData(data: data) { name in let dir = atlasURL.deletingLastPathComponent() let file = dir.appendingPathComponent(name) return try await FileSource.http(file).load() } } private static func fromData(data: Data, loadFile: (_ name: String) async throws -> Data) async throws -> (Atlas, [UIImage]) { guard let atlasData = String(data: data, encoding: .utf8) else { throw SpineError("Couldn't read atlas bytes as utf8 string") } let atlas = try atlasData.utf8CString.withUnsafeBufferPointer { guard let atlas = spine_atlas_load($0.baseAddress) else { throw SpineError("Couldn't load atlas data") } return atlas } if let error = spine_atlas_get_error(atlas) { let message = String(cString: error) spine_atlas_dispose(atlas) throw SpineError("Couldn't load atlas: \(message)") } var atlasPages = [UIImage]() let numImagePaths = spine_atlas_get_num_image_paths(atlas); for i in 0.. SkeletonData { return try fromData( atlas: atlas, data: try await FileSource.bundle(fileName: skeletonFileName, bundle: bundle).load(), isJson: skeletonFileName.hasSuffix(".json") ) } /// Loads a ``SkeletonData`` from the file URL `skeletonFile`. Uses the provided ``Atlas`` to resolve attachment images. /// /// Throws an `Error` in case the skeleton data could not be loaded. static func fromFile(atlas: Atlas, skeletonFile: URL) async throws -> SkeletonData { return try fromData( atlas: atlas, data: try await FileSource.file(skeletonFile).load(), isJson: skeletonFile.absoluteString.hasSuffix(".json") ) } /// Loads a ``SkeletonData`` from the http URL `skeletonFile`. Uses the provided ``Atlas`` to resolve attachment images. /// /// Throws an `Error` in case the skeleton data could not be loaded. static func fromHttp(atlas: Atlas, skeletonURL: URL) async throws -> SkeletonData { return try fromData( atlas: atlas, data: try await FileSource.http(skeletonURL).load(), isJson: skeletonURL.absoluteString.hasSuffix(".json") ) } /// Loads a ``SkeletonData`` from the ``binary`` skeleton `Data`, using the provided ``Atlas`` to resolve attachment images. /// /// Throws an `Error` in case the skeleton data could not be loaded. static func fromData(atlas: Atlas, data: Data) throws -> SkeletonData { let result = try data.withUnsafeBytes{ try $0.withMemoryRebound(to: UInt8.self) { buffer in 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) ) } } guard let result else { throw SpineError("Couldn't load skeleton data") } defer { spine_skeleton_data_result_dispose(result) } if let error = spine_skeleton_data_result_get_error(result) { let message = String(cString: error) throw SpineError("Couldn't load skeleton data: \(message)") } guard let data = spine_skeleton_data_result_get_data(result) else { throw SpineError("Couldn't load skeleton data from result") } return SkeletonData(data) } /// Loads a ``SkeletonData`` from the `json` string, using the provided ``Atlas`` to resolve attachment /// images. /// /// Throws an `Error` in case the atlas could not be loaded. static func fromJson(atlas: Atlas, json: String) throws -> SkeletonData { let result = try json.utf8CString.withUnsafeBufferPointer { buffer in guard 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) { let message = String(cString: error) throw SpineError("Couldn't load skeleton data: \(message)") } guard let data = spine_skeleton_data_result_get_data(result) else { throw SpineError("Couldn't load skeleton data from result") } return SkeletonData(data) } private static func fromData(atlas: Atlas, data: Data, isJson: Bool) throws -> SkeletonData { if isJson { guard let json = String(data: data, encoding: .utf8) else { throw SpineError("Couldn't read skeleton data json string") } return try fromJson(atlas: atlas, json: json) } else { return try fromData(atlas: atlas, data: data) } } } internal extension SkeletonDrawable { func render() -> [RenderCommand] { var commands = [RenderCommand]() if disposed { return commands } var nativeCmd = spine_skeleton_drawable_render(wrappee) repeat { if let ncmd = nativeCmd { commands.append(RenderCommand(ncmd)) nativeCmd = spine_render_command_get_next(ncmd) } else { nativeCmd = nil } } while (nativeCmd != nil) return commands } } internal extension RenderCommand { var numVertices: Int { Int(spine_render_command_get_num_vertices(wrappee)) } func positions(numVertices: Int) -> [Float] { let num = numVertices * 2 let ptr = spine_render_command_get_positions(wrappee) guard let validPtr = ptr else { return [] } let buffer = UnsafeBufferPointer(start: validPtr, count: num) return Array(buffer) } func uvs(numVertices: Int) -> [Float] { let num = numVertices * 2 let ptr = spine_render_command_get_uvs(wrappee) guard let validPtr = ptr else { return [] } let buffer = UnsafeBufferPointer(start: validPtr, count: num) return Array(buffer) } func colors(numVertices: Int) ->[Int32] { let num = numVertices let ptr = spine_render_command_get_colors(wrappee) guard let validPtr = ptr else { return [] } let buffer = UnsafeBufferPointer(start: validPtr, count: num) return Array(buffer) } } public extension Skin { /// Constructs a new empty ``Skin`` using the given `name`. Skins constructed this way must be manually disposed via the `dispose` method /// if they are no longer used. static func create(name: String) -> Skin { return Skin(spine_skin_create(name)) } } // Helper public extension CGRect { /// Construct a `CGRect` from ``Bounds`` init(bounds: Bounds) { self = CGRect( x: CGFloat(bounds.x), y: CGFloat(bounds.y), width: CGFloat(bounds.width), height: CGFloat(bounds.height) ) } } internal enum FileSource { case bundle(fileName: String, bundle: Bundle = .main) case file(URL) case http(URL) internal func load() async throws -> Data { switch self { case .bundle(let fileName, let bundle): let components = fileName.split(separator: ".") guard components.count > 1, let ext = components.last else { throw SpineError("Provide both file name and file extension") } let name = components.dropLast(1).joined(separator: ".") guard let fileUrl = bundle.url(forResource: name, withExtension: String(ext)) else { throw SpineError("Could not load file with name \(name) from bundle") } return try Data(contentsOf: fileUrl, options: []) case .file(let fileUrl): return try Data(contentsOf: fileUrl, options: []) case .http(let url): if #available(iOS 15.0, *) { let (temp, response) = try await URLSession.shared.download(from: url) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw URLError(.badServerResponse) } return try Data(contentsOf: temp, options: []) } else { let lock = NSRecursiveLock() nonisolated(unsafe) var isCancelled = false nonisolated(unsafe) var taskHolder:URLSessionDownloadTask? = nil return try await withTaskCancellationHandler { try await withCheckedThrowingContinuation { continuation in let task = URLSession.shared.downloadTask(with: url) { temp, response, error in if let 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() } } } onCancel: { lock.withLock { isCancelled = true let value = taskHolder taskHolder = nil return value }?.cancel() } } } } } public struct SpineError: Error, CustomStringConvertible { public let description: String internal init(_ description: String) { self.description = description } } public extension SkeletonBounds { static func create() -> SkeletonBounds { return SkeletonBounds(spine_skeleton_bounds_create()) } } @objc public extension Atlas { var imagePathCount:Int32 { spine_atlas_get_num_image_paths(wrappee) } }