spine-runtimes/spine-ios/Sources/Spine/Spine.Generated+Extensions.swift
Denis Andrašec 0d5c3e3b18
Spine iOS (#2504)
* Add `spine-iOS` SPM package & example app (#1)

* Basic Mesh Rendering (#2)

* Spine C++ Swift Wrapper (#3)

* Load `Atlas` & `SkeletonData` (#4)

Load & dispose `Atlas` & `SkeletonData` from bundled files.

* Generate Swift classes from `spine-cpp-lite.h` (#5)

* Draw `SkeletonData` render commands (#6)

- Use `SkeletonData` render commands in the renderer
- Simple loop for animation support

* Add `BoundsProvider` (#7)

- Implement & support `BoundsProvider` classes
- Introduce alignment and content mode
- Update c to swift script to return optional for find prefixed methods

* Support `SpineController` & `Event` callbacks (#8)

- Support SpineController callbacks
- Support Event callbacks
- Apply tint color in renderer

* Support `DressUp` sample (#9)

- Add `DressUp` sample
- Move SpineViewController to SpineUIView
- Implement SpineUIView export to image

* Remove unused file

* Add `Physics` sample (#10)

- Add `Physics` sample
- Fix offsets in `IKFollowing` sample
- Fix `SpineView` background color

* Add `DebugRendering` sample (#11)

- Add `DebugRendering` sample
- Make `SpineUIView` transparent

* Move remaining files to SPM package (#12)

- Move remaining files to SPM package
- Rename `SpineWrapper` to `SpineCppLite`

* Load assets from different sources (#13)

- Load from bundle, file, http & drawable
- Apply correct blend mode & pma in renderer

* Add `Obj-C` + `UIKit` sample (#14)

- Add `Obj-C` + `UIKit` sample
- Update `Spine` to be usable in Obj-C code base

* Support CocoaPods (#15)

* Metal Best Practices (#16)

- Tripple Buffering
- Buffer Bindings
- Shared Objects

* Annotate functions that should return optional (#17)

* Add option to disable drawing when out of viewport (#18)

- Add option to disable drawing when out of viewport
- Move update clock to controller so multiple views can share it

* Add docs for public Spine classes/methods (#19)

* Fix various regressions (#20)

- Fix retain `SpineController` retain cycle
- Fix issue wehre images were not rendered
2024-06-18 10:02:25 +02:00

316 lines
12 KiB
Swift

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) as? NSString else {
throw "Couldn't read atlas bytes as utf8 string"
}
let atlasDataNative = UnsafeMutablePointer<CChar>(mutating: atlasData.utf8String)
guard let atlas = spine_atlas_load(atlasDataNative) else {
throw "Couldn't load atlas data"
}
if let error = spine_atlas_get_error(atlas) {
let message = String(cString: error)
spine_atlas_dispose(atlas)
throw "Couldn't load atlas: \(message)"
}
var atlasPages = [UIImage]()
let numImagePaths = spine_atlas_get_num_image_paths(atlas);
for i in 0..<numImagePaths {
guard let atlasPageFilePointer = spine_atlas_get_image_path(atlas, i) else {
continue
}
let atlasPageFile = String(cString: atlasPageFilePointer)
let imageData = try await loadFile(atlasPageFile)
guard let image = UIImage(data: imageData) else {
continue
}
atlasPages.append(image)
}
return (Atlas(atlas), atlasPages)
}
}
public extension SkeletonData {
/// Loads a ``SkeletonData`` from the file with name `skeletonFileName` in the main bundle or the optionally provided `bundle`.
/// Uses the provided ``Atlas`` to resolve attachment images.
///
/// Throws an `Error` in case the skeleton data could not be loaded.
static func fromBundle(atlas: Atlas, skeletonFileName: String, bundle: Bundle = .main) async throws -> 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 binaryNative = try data.withUnsafeBytes { unsafeBytes in
guard let bytes = unsafeBytes.bindMemory(to: UInt8.self).baseAddress else {
throw "Couldn't read atlas binary"
}
return (data: bytes, length: Int32(unsafeBytes.count))
}
let result = spine_skeleton_data_load_binary(
atlas.wrappee,
binaryNative.data,
binaryNative.length
)
if let error = spine_skeleton_data_result_get_error(result) {
let message = String(cString: error)
spine_skeleton_data_result_dispose(result)
throw "Couldn't load skeleton data: \(message)"
}
guard let data = spine_skeleton_data_result_get_data(result) else {
throw "Couldn't load skeleton data from result"
}
spine_skeleton_data_result_dispose(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 jsonNative = UnsafeMutablePointer<CChar>(mutating: (json as NSString).utf8String)
guard let result = spine_skeleton_data_load_json(atlas.wrappee, jsonNative) else {
throw "Couldn't load skeleton data json"
}
if let error = spine_skeleton_data_result_get_error(result) {
let message = String(cString: error)
spine_skeleton_data_result_dispose(result)
throw "Couldn't load skeleton data: \(message)"
}
guard let data = spine_skeleton_data_result_get_data(result) else {
throw "Couldn't load skeleton data from result"
}
spine_skeleton_data_result_dispose(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 "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)
return (0..<num).compactMap { ptr?[$0] }
}
func uvs(numVertices: Int) -> [Float] {
let num = numVertices * 2
let ptr = spine_render_command_get_uvs(wrappee)
return (0..<num).compactMap { ptr?[$0] }
}
func colors(numVertices: Int) ->[Int32] {
let num = numVertices
let ptr = spine_render_command_get_colors(wrappee)
return (0..<num).compactMap { ptr?[$0] }
}
}
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 "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 "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 {
return 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: "Could not download file.")
return
}
do {
continuation.resume(returning: try Data(contentsOf: temp, options: []))
} catch {
continuation.resume(throwing: error)
}
}
}
task.resume()
}
}
}
}
}
extension String: Error {
}