浏览代码

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
Denis Andrašec 1 年之前
父节点
当前提交
0d5c3e3b18
共有 89 个文件被更改,包括 10369 次插入13 次删除
  1. 39 0
      Package.swift
  2. 27 0
      Spine.podspec
  3. 25 0
      SpineCppLite.podspec
  4. 24 0
      SpineShadersStructs.podspec
  5. 4 0
      spine-cpp/spine-cpp-lite/module.modulemap
  6. 556 0
      spine-cpp/spine-cpp-lite/spine-cpp-lite-codegen.py
  7. 20 4
      spine-cpp/spine-cpp-lite/spine-cpp-lite.cpp
  8. 53 4
      spine-cpp/spine-cpp-lite/spine-cpp-lite.h
  9. 2 2
      spine-flutter/example/lib/animation_state_events.dart
  10. 0 1
      spine-flutter/example/lib/physics.dart
  11. 1 1
      spine-flutter/lib/spine_widget.dart
  12. 1 1
      spine-flutter/pubspec.yaml
  13. 8 0
      spine-ios/.gitignore
  14. 24 0
      spine-ios/CHANGELOG.md
  15. 92 0
      spine-ios/Example - Cocoapods/.gitignore
  16. 11 0
      spine-ios/Example - Cocoapods/Podfile
  17. 455 0
      spine-ios/Example - Cocoapods/Spine iOS Example.xcodeproj/project.pbxproj
  18. 11 0
      spine-ios/Example - Cocoapods/Spine iOS Example/Assets.xcassets/AccentColor.colorset/Contents.json
  19. 13 0
      spine-ios/Example - Cocoapods/Spine iOS Example/Assets.xcassets/AppIcon.appiconset/Contents.json
  20. 6 0
      spine-ios/Example - Cocoapods/Spine iOS Example/Assets.xcassets/Contents.json
  21. 6 0
      spine-ios/Example - Cocoapods/Spine iOS Example/Preview Content/Preview Assets.xcassets/Contents.json
  22. 35 0
      spine-ios/Example - Cocoapods/Spine iOS Example/SimpleAnimation.swift
  23. 18 0
      spine-ios/Example - Cocoapods/Spine iOS Example/Spine_iOS_ExampleApp.swift
  24. 557 0
      spine-ios/Example - Cocoapods/Spine iOS Example/spineboy/spineboy-pro.json
  25. 二进制
      spine-ios/Example - Cocoapods/Spine iOS Example/spineboy/spineboy-pro.skel
  26. 94 0
      spine-ios/Example - Cocoapods/Spine iOS Example/spineboy/spineboy.atlas
  27. 二进制
      spine-ios/Example - Cocoapods/Spine iOS Example/spineboy/spineboy.png
  28. 91 0
      spine-ios/Example/.gitignore
  29. 557 0
      spine-ios/Example/Spine iOS Example.xcodeproj/project.pbxproj
  30. 7 0
      spine-ios/Example/Spine iOS Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  31. 48 0
      spine-ios/Example/Spine iOS Example/AnimationStateEvents.swift
  32. 11 0
      spine-ios/Example/Spine iOS Example/Assets.xcassets/AccentColor.colorset/Contents.json
  33. 63 0
      spine-ios/Example/Spine iOS Example/Assets.xcassets/AppIcon.appiconset/Contents.json
  34. 6 0
      spine-ios/Example/Spine iOS Example/Assets.xcassets/Contents.json
  35. 二进制
      spine-ios/Example/Spine iOS Example/Assets/celestial/celestial-circus-pro.skel
  36. 173 0
      spine-ios/Example/Spine iOS Example/Assets/celestial/celestial-circus.atlas
  37. 二进制
      spine-ios/Example/Spine iOS Example/Assets/celestial/celestial-circus.png
  38. 二进制
      spine-ios/Example/Spine iOS Example/Assets/dragon/dragon-ess.skel
  39. 112 0
      spine-ios/Example/Spine iOS Example/Assets/dragon/dragon.atlas
  40. 二进制
      spine-ios/Example/Spine iOS Example/Assets/dragon/dragon.png
  41. 二进制
      spine-ios/Example/Spine iOS Example/Assets/dragon/dragon_2.png
  42. 二进制
      spine-ios/Example/Spine iOS Example/Assets/dragon/dragon_3.png
  43. 二进制
      spine-ios/Example/Spine iOS Example/Assets/dragon/dragon_4.png
  44. 二进制
      spine-ios/Example/Spine iOS Example/Assets/dragon/dragon_5.png
  45. 二进制
      spine-ios/Example/Spine iOS Example/Assets/mixandmatch/mix-and-match-pro.skel
  46. 358 0
      spine-ios/Example/Spine iOS Example/Assets/mixandmatch/mix-and-match.atlas
  47. 二进制
      spine-ios/Example/Spine iOS Example/Assets/mixandmatch/mix-and-match.png
  48. 557 0
      spine-ios/Example/Spine iOS Example/Assets/spineboy/spineboy-pro.json
  49. 二进制
      spine-ios/Example/Spine iOS Example/Assets/spineboy/spineboy-pro.skel
  50. 94 0
      spine-ios/Example/Spine iOS Example/Assets/spineboy/spineboy.atlas
  51. 二进制
      spine-ios/Example/Spine iOS Example/Assets/spineboy/spineboy.png
  52. 76 0
      spine-ios/Example/Spine iOS Example/DebugRendering.swift
  53. 61 0
      spine-ios/Example/Spine iOS Example/DisableRendering.swift
  54. 129 0
      spine-ios/Example/Spine iOS Example/DressUp.swift
  55. 73 0
      spine-ios/Example/Spine iOS Example/IKFollowing.swift
  56. 61 0
      spine-ios/Example/Spine iOS Example/MainView.swift
  57. 77 0
      spine-ios/Example/Spine iOS Example/Physics.swift
  58. 45 0
      spine-ios/Example/Spine iOS Example/PlayPauseAnimation.swift
  59. 35 0
      spine-ios/Example/Spine iOS Example/SimpleAnimation.swift
  60. 9 0
      spine-ios/Example/Spine iOS Example/SimpleAnimationViewController.h
  61. 44 0
      spine-ios/Example/Spine iOS Example/SimpleAnimationViewController.m
  62. 13 0
      spine-ios/Example/Spine iOS Example/SimpleAnimationViewControllerRepresentable.swift
  63. 5 0
      spine-ios/Example/Spine iOS Example/Spine iOS Example-Bridging-Header.h
  64. 14 0
      spine-ios/Example/Spine iOS Example/SpineExampleApp.swift
  65. 10 0
      spine-ios/Example/Spine iOS Example/SpineiOSExample.entitlements
  66. 6 0
      spine-ios/README.md
  67. 70 0
      spine-ios/Sources/Spine/AnimationStateWrapper.swift
  68. 157 0
      spine-ios/Sources/Spine/BoundsProvider.swift
  69. 15 0
      spine-ios/Sources/Spine/Extensions/MTLClearColor+UIColor.swift
  70. 48 0
      spine-ios/Sources/Spine/Extensions/RenderCommand+Vertices.swift
  71. 54 0
      spine-ios/Sources/Spine/Extensions/SkeletonDrawableWrapper+CGImage.swift
  72. 19 0
      spine-ios/Sources/Spine/Metal/SpineObjects.swift
  73. 322 0
      spine-ios/Sources/Spine/Metal/SpineRenderer.swift
  74. 70 0
      spine-ios/Sources/Spine/Metal/SpineShaders.metal
  75. 145 0
      spine-ios/Sources/Spine/SkeletonDrawableWrapper.swift
  76. 315 0
      spine-ios/Sources/Spine/Spine.Generated+Extensions.swift
  77. 3721 0
      spine-ios/Sources/Spine/Spine.Generated.swift
  78. 215 0
      spine-ios/Sources/Spine/SpineController.swift
  79. 280 0
      spine-ios/Sources/Spine/SpineUIView.swift
  80. 76 0
      spine-ios/Sources/Spine/SpineView.swift
  81. 4 0
      spine-ios/Sources/SpineCppLite/include/module.modulemap
  82. 1 0
      spine-ios/Sources/SpineCppLite/include/spine
  83. 1 0
      spine-ios/Sources/SpineCppLite/include/spine-cpp-lite.h
  84. 1 0
      spine-ios/Sources/SpineCppLite/spine
  85. 1 0
      spine-ios/Sources/SpineCppLite/spine-cpp-lite.cpp
  86. 1 0
      spine-ios/Sources/SpineShadersStructs/SpineShadersStructs.cpp
  87. 28 0
      spine-ios/Sources/SpineShadersStructs/SpineShadersStructs.h
  88. 4 0
      spine-ios/Sources/SpineShadersStructs/module.modulemap
  89. 4 0
      spine-ios/setup.sh

+ 39 - 0
Package.swift

@@ -0,0 +1,39 @@
+// swift-tools-version: 5.9
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let package = Package(
+    name: "spine-ios",
+    platforms: [
+        .iOS(.v13)
+    ],
+    products: [
+        // Products define the executables and libraries a package produces, making them visible to other packages.
+        .library(
+            name: "Spine",
+            targets: ["Spine"]
+        )
+    ],
+    targets: [
+        .target(
+            name: "Spine",
+            dependencies: [
+                "SpineCppLite", "SpineShadersStructs"
+            ],
+            path: "spine-ios/Sources/Spine",
+            swiftSettings: [
+                .interoperabilityMode(.Cxx)
+            ]
+        ),
+        .target(
+            name: "SpineCppLite",
+            path: "spine-ios/Sources/SpineCppLite"
+        ),
+        .systemLibrary(
+            name: "SpineShadersStructs",
+            path: "spine-ios/Sources/SpineShadersStructs"
+        )
+    ],
+    cxxLanguageStandard: .cxx11
+)

+ 27 - 0
Spine.podspec

@@ -0,0 +1,27 @@
+#
+# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
+# Run `pod lib lint spine_flutter.podspec` to validate before publishing.
+#
+Pod::Spec.new do |s|
+  s.name             = 'Spine'
+  s.version          = '0.0.1'
+  s.summary          = 'Spine runtimes for iOS.'
+  s.description      = <<-DESC
+Spine runtimes for iOS.
+                       DESC
+  s.homepage         = 'https://esotericsoftware.com'
+  s.author           = { "Esoteric Software LLC  " => "https://esotericsoftware.com" }
+  s.license          = { :file => 'LICENSE' }
+
+  s.source           = { :git => 'https://github.com/denrase/spine-runtimes.git', :branch => 'cocoapods' }
+  s.source_files     = 'spine-ios/Sources/Spine/**/*.{swift,metal}'
+  s.platform         = :ios, '13.0'
+  
+  s.xcconfig = { 
+    'HEADER_SEARCH_PATHS' => '"$(PODS_ROOT)/SpineCppLite/spine-cpp/spine-cpp/include" "$(PODS_ROOT)/SpineCppLite/spine-cpp/spine-cpp-lite"'
+  }
+
+  s.swift_version = '5.0'
+  s.dependency 'SpineCppLite'
+  s.dependency 'SpineShadersStructs'
+end

+ 25 - 0
SpineCppLite.podspec

@@ -0,0 +1,25 @@
+#
+# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
+# Run `pod lib lint spine_flutter.podspec` to validate before publishing.
+#
+Pod::Spec.new do |s|
+  s.name                = 'SpineCppLite'
+  s.version             = '0.0.1'
+  s.summary             = 'Spine runtimes for iOS.'
+  s.description         = <<-DESC
+Spine runtimes for iOS.
+                       DESC
+  s.homepage            = 'https://esotericsoftware.com'
+  s.author              = { "Esoteric Software LLC  " => "https://esotericsoftware.com" }
+  s.license             = { :file => 'LICENSE' }
+  s.platform            = :ios, '13.0'
+
+  s.source              = { :git => 'https://github.com/denrase/spine-runtimes.git', :branch => 'cocoapods' }
+  s.source_files        =  'spine-cpp/spine-cpp/**/*.{h,cpp}', 'spine-cpp/spine-cpp-lite/*.{h,cpp}'
+  s.module_map          = 'spine-cpp/spine-cpp-lite/module.modulemap'
+  s.pod_target_xcconfig = {
+    'HEADER_SEARCH_PATHS' => '"$(PODS_ROOT)/SpineCppLite/spine-cpp/spine-cpp/include" "$(PODS_ROOT)/SpineCppLite/spine-cpp/spine-cpp-lite"',
+    'CLANG_CXX_LANGUAGE_STANDARD' => 'c++11',
+    'CLANG_CXX_LIBRARY' => 'libc++'
+  }
+end

+ 24 - 0
SpineShadersStructs.podspec

@@ -0,0 +1,24 @@
+#
+# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
+# Run `pod lib lint spine_flutter.podspec` to validate before publishing.
+#
+Pod::Spec.new do |s|
+  s.name                = 'SpineShadersStructs'
+  s.version             = '0.0.1'
+  s.summary             = 'Metal shaders structs for spine'
+  s.description         = <<-DESC
+Metal shaders structs for spine.
+                       DESC
+  s.homepage            = 'https://esotericsoftware.com'
+  s.author              = { "Esoteric Software LLC  " => "https://esotericsoftware.com" }
+  s.license             = { :file => 'LICENSE' }
+  s.platform            = :ios, '13.0'
+
+  s.source              = { :git => 'https://github.com/denrase/spine-runtimes.git', :branch => 'cocoapods' }
+  s.source_files        = 'spine-ios/Sources/SpineShadersStructs/*.{h,cpp}'
+
+  s.pod_target_xcconfig = {
+    'CLANG_CXX_LANGUAGE_STANDARD' => 'c++11',
+    'CLANG_CXX_LIBRARY' => 'libc++'
+  }
+end

+ 4 - 0
spine-cpp/spine-cpp-lite/module.modulemap

@@ -0,0 +1,4 @@
+module SpineCppLite {
+    header "spine-cpp-lite.h"
+    export *
+}

+ 556 - 0
spine-cpp/spine-cpp-lite/spine-cpp-lite-codegen.py

@@ -0,0 +1,556 @@
+import re
+import os
+
+script_directory = os.path.dirname(os.path.abspath(__file__))
+input_path = os.path.join(script_directory, 'spine-cpp-lite.h')
+
+with open(input_path, 'r') as file:
+    file_contents = file.read()
+
+supported_types_to_swift_types = {
+    'void *': 'UnsafeMutableRawPointer',
+    'const utf8 *': 'String?',
+    'uint64_t': 'UInt64',
+    'float *': 'Float?',
+    'float': 'Float',
+    'int32_t': 'Int32',
+    'utf8 *': 'String?',
+    'int32_t *': 'Int32?',
+    'uint16_t *': 'UInt16',
+    'spine_bool': 'Bool'
+}
+
+def read_spine_types(data):
+    types_start = data.find('// @start: opaque_types') + len('// @start: opaque_types')
+    types_end = data.find('// @end: paque_types')
+    types_section = data[types_start:types_end]
+    return re.findall(r'SPINE_OPAQUE_TYPE\(([^)]+)\)', types_section)
+
+def read_spine_function_declarations(data):
+    declarations_start = data.find('// @start: function_declarations') + len('// @start: function_declarations')
+    declarations_end = data.find('// @end: function_declarations')
+    declarations_section = data[declarations_start:declarations_end]
+    lines = declarations_section.split('\n')
+
+    filtered_lines = []
+    ignore_next = False
+    next_returns_optional = False
+    for line in lines:
+      if ignore_next:
+         ignore_next = False
+         continue
+      
+      line = line.strip()
+
+      if next_returns_optional:
+        next_returns_optional = False
+        line = line + "?"
+      
+      if not line.strip().startswith('//') and line.strip() != '':
+        filtered_lines.append(line)
+
+      if line.startswith('//') and '@ignore' in line:
+        ignore_next = True
+      elif line.startswith('//') and '@optional' in line:
+        next_returns_optional = True
+    
+    function_declaration = [
+        line.replace('SPINE_CPP_LITE_EXPORT', '').strip()
+        for line in filtered_lines
+    ]
+
+    return function_declaration
+
+def read_spine_enums(data):
+    enums_start = data.find('// @start: enums') + len('// @start: enums')
+    enums_end = data.find('// @end: enums')
+    enums_section = data[enums_start:enums_end]
+    return re.findall(r"typedef enum (\w+) \{", enums_section)
+
+class SpineObject:
+    def __init__(self, name, functions):
+        self.name = name
+        self.functions = functions
+        self.function_names = {function.name for function in functions}
+        self.var_name = "wrappee"
+
+    def __str__(self):
+        return f"SpineObject: name: {self.name}, functions: {self.functions}"
+    
+class SpineFunction:
+    def __init__(self, return_type, name, parameters, returns_optional):
+        self.return_type = return_type
+        self.name = name
+        self.parameters = parameters
+        self.returns_optional = returns_optional
+
+    def isReturningSpineClass(self):
+       return self.return_type.startswith("spine_") and self.return_type != "spine_bool"  and self.return_type not in enums
+
+    def __str__(self):
+        return f"SpineFunction(return_type: {self.return_type}, name: {self.name}, parameters: {self.parameters}, returns_optional: {self.returns_optional})"
+    
+    def __repr__(self):
+        return self.__str__()
+
+class SpineParam:
+    def __init__(self, type, name):
+        self.type = type
+        self.name = name
+
+    def isSpineClass(self):
+       return self.type.startswith("spine_") and self.type != "spine_bool" and self.type not in enums
+
+    def __str__(self):
+        return f"SpineParam(type: {self.type}, name: {self.name})"
+    
+    def __repr__(self):
+        return self.__str__()
+
+def parse_function_declaration(declaration):
+    returns_optional = declaration.endswith("?")
+
+    # Strip semicolon and extra whitespace
+    declaration = declaration.strip('?').strip(';').strip()
+    
+    # Use regex to split the declaration into parts
+    # Regex explanation:
+  # ^([\w\s\*]+?)\s+ - Capture the return type, possibly including spaces and asterisks (non-greedy)
+    # ([\w]+) - Capture the function name (alphanumeric and underscores)
+    # \((.*)\) - Capture the argument list in entirety
+    match = re.match(r'^(\S.+?\s*\*?\s*)([\w]+)\s*\((.*)\)$', declaration)
+
+    if not match:
+        return "Invalid function declaration"
+    
+    return_type, function_name, params = match.groups()
+
+    params = params.strip()
+    parameters = []
+    if params:
+        # Splitting each argument on comma
+        param_list = params.split(',')
+        for param in param_list:
+            
+            param_parts = []
+            if '*' in param: # Split at the pointer and add it as a suffix to the type
+              param_parts = param.rsplit('*', 1)
+              param_parts[0] = param_parts[0] + '*'
+            else: # Assuming type and name are separated by space and taking the last space as the separator
+              param_parts = param.rsplit(' ', 1)
+            param_type, param_name = param_parts
+            spine_param = SpineParam(type = param_type.strip(), name = param_name.strip())
+            parameters.append(spine_param)
+    
+    return SpineFunction(
+        return_type = return_type.strip(),
+        name = function_name.strip(),
+        parameters = parameters,
+        returns_optional = returns_optional
+    )
+
+types = read_spine_types(file_contents)
+function_declarations = read_spine_function_declarations(file_contents)
+enums = read_spine_enums(file_contents)
+
+sorted_types = sorted(types, key=len, reverse=True) # Sorted by legth descending so we can match longest prefix.
+spine_functions = [
+    parse_function_declaration(function_declaration)
+    for function_declaration in function_declarations
+]
+
+objects = []
+
+for type in sorted_types:
+    object_functions = []
+
+    hits = set() ## Keep track of hits and remove them for next object
+
+    for function_declaration in function_declarations:
+          spine_function = parse_function_declaration(function_declaration)
+          if spine_function.name.startswith(type):
+              hits.add(function_declaration);
+              object_functions.append(spine_function);
+    
+    object = SpineObject(name = type, functions = object_functions);
+    objects.append(object)
+
+    function_declarations = [item for item in function_declarations if item not in hits]
+
+def snake_to_camel(snake_str):
+    # Split the string by underscore
+    parts = snake_str.split('_')
+    # Return the first part lowercased and concatenate capitalized subsequent parts
+    return parts[0] + ''.join(word.capitalize() for word in parts[1:])
+
+def snake_to_title(snake_str):
+    # Split the string at underscores
+    words = snake_str.split('_')
+    # Capitalize the first letter of each word
+    words = [word.capitalize() for word in words]
+    # Join the words into a single string without any separator
+    title_str = ''.join(words)
+    return title_str
+
+inset = "    "
+
+class SwiftTypeWriter:
+    def __init__(self, type):
+        self.type = type
+        
+    def write(self):
+        parameter_type = supported_types_to_swift_types.get(self.type)
+        if parameter_type is None:
+          parameter_type = snake_to_title(self.type.replace("spine_", ""))
+        
+        if parameter_type.endswith(" *"):
+            parameter_type = f"{parameter_type[:-2]}"
+        
+        return parameter_type
+        
+class SwiftParamWriter:
+    def __init__(self, param):
+        self.param = param
+        
+    def write(self):
+        type = SwiftTypeWriter(type = self.param.type).write()
+        return f"{snake_to_camel(self.param.name)}: {type}"
+
+class SwiftFunctionBodyWriter:
+  def __init__(self, spine_object, spine_function, is_setter, is_getter_optional):
+      self.spine_object = spine_object
+      self.spine_function = spine_function
+      self.is_setter = is_setter
+      self.is_getter_optional = is_getter_optional
+
+  def write(self):
+    body = ""
+
+    num_function_name = self.spine_function.name.replace("get_", "get_num_")
+    swift_return_type_is_array = "get_" in self.spine_function.name and num_function_name in self.spine_object.function_names
+    
+    spine_params = self.spine_function.parameters;
+
+    body = ""
+    if "dispose" in self.spine_function.name:
+       body += self.write_dispose_call()
+    
+    function_call = self.write_c_function_call(spine_params)
+
+    if swift_return_type_is_array:
+      body += self.write_array_call(num_function_name, function_call)
+      body += inset + inset
+      body += "}"
+    else:
+      if not self.spine_function.return_type == "void":
+        body += "return "
+
+      if self.spine_function.isReturningSpineClass():
+
+        function_prefix = f"{self.spine_object.name}_"
+        function_name = self.spine_function.name.replace(function_prefix, "", 1)
+
+        if "find_" in function_name or self.spine_function.returns_optional:
+          body += function_call
+          body += ".flatMap { .init($0"
+          if self.spine_function.return_type in enums:
+            body += ".rawValue"
+          body += ") }"
+        else:
+          body += ".init("
+          body += function_call
+          if self.spine_function.return_type in enums:
+            body += ".rawValue"
+          body += ")"
+          
+      else:
+        body += function_call
+      
+      if self.spine_function.return_type == "const utf8 *" or self.spine_function.return_type == "utf8 *":
+        body += ".flatMap { String(cString: $0) }"
+      if self.spine_function.return_type == "int32_t *" or self.spine_function.return_type == "float *":
+        body += ".flatMap { $0.pointee }"
+
+    return body
+  
+  def write_c_function_call(self, spine_params):
+      function_call = ""
+      function_call += f"{self.spine_function.name}"
+      function_call += "("
+
+      # Replace name with ivar name
+      spine_params_with_ivar_name = spine_params
+      if spine_params_with_ivar_name and spine_params_with_ivar_name[0].type == self.spine_object.name:
+        spine_params_with_ivar_name[0].name = self.spine_object.var_name
+      
+      if self.is_setter and len(spine_params_with_ivar_name) == 2:
+        spine_params_with_ivar_name[1].name = "newValue"
+        if self.is_getter_optional:
+            spine_params_with_ivar_name[1].name += "?"
+      
+      swift_param_names = []
+      for idx, spine_param in enumerate(spine_params_with_ivar_name):
+        if spine_param.isSpineClass() and idx > 0:
+            swift_param_names.append(f"{spine_param.name}.wrappee")
+        elif spine_param.type == "spine_bool":
+           swift_param_names.append(f"{spine_param.name} ? -1 : 0")
+        else:
+           swift_param_names.append(spine_param.name)
+         
+
+      function_call += ", ".join(swift_param_names)
+      function_call += ")"
+
+      if self.spine_function.return_type == "spine_bool":
+         function_call += " != 0"
+
+      return function_call
+    
+  def write_array_call(self, num_function_name, function_call):
+    array_call = f"let num = Int({num_function_name}({self.spine_object.var_name}))"
+    array_call += "\n"
+    array_call += inset + inset
+    array_call += f"let ptr = {function_call}"
+    array_call += "\n"
+    array_call += inset + inset
+    array_call += "return (0..<num).compactMap {"
+    array_call += "\n"
+    array_call += inset + inset + inset
+
+    if self.spine_function.isReturningSpineClass():
+        array_call += "ptr?[$0].flatMap { .init($0) }" 
+    else:
+      array_call += "ptr?[$0]"
+
+    array_call += "\n"
+    return array_call
+  
+  def write_dispose_call(self):
+     dispose_body = "if disposed { return }"
+     dispose_body += "\n"
+     dispose_body += inset + inset
+     dispose_body += "disposed = true"
+     dispose_body += "\n"
+     dispose_body += inset + inset
+     return dispose_body
+
+class SwiftFunctionWriter:
+    def __init__(self, spine_object, spine_function, spine_setter_function):
+        self.spine_object = spine_object
+        self.spine_function = spine_function
+        self.spine_setter_function = spine_setter_function
+
+    def write(self):
+        function_prefix = f"{self.spine_object.name}_"
+        function_name = self.spine_function.name.replace(function_prefix, "", 1)
+        is_getter = (function_name.startswith("get_") or function_name.startswith("is_")) and len(self.spine_function.parameters) < 2
+
+        num_function_name = self.spine_function.name.replace("get_", "get_num_")
+        swift_return_type_is_array = "get_" in self.spine_function.name and num_function_name in self.spine_object.function_names
+
+        swift_return_type_writer = SwiftTypeWriter(type = self.spine_function.return_type)
+        swift_return_type = swift_return_type_writer.write()
+        if swift_return_type_is_array:
+           swift_return_type = f"[{swift_return_type}]"
+
+        function_string = inset
+
+        if is_getter:
+          
+          function_string += self.write_computed_property_signature(function_name, swift_return_type)
+          if self.spine_setter_function:
+             function_string += " {\n"
+             function_string += inset + inset
+             function_string += "get"
+
+        else:
+          function_string += self.write_method_signature(function_name, swift_return_type)
+        
+        function_string += " {"
+        function_string += "\n"
+
+        function_string += inset + inset
+
+        if self.spine_setter_function:
+          function_string += inset
+        
+        function_string += SwiftFunctionBodyWriter(spine_object = self.spine_object, spine_function = self.spine_function, is_setter=False, is_getter_optional=False).write()
+
+        if self.spine_setter_function:
+           function_string += "\n"
+           function_string += inset + inset + "}"
+           function_string += "\n"
+           function_string += inset + inset + "set {"
+           function_string += "\n"
+           function_string += inset + inset + inset
+           function_string += SwiftFunctionBodyWriter(spine_object = self.spine_object, spine_function = self.spine_setter_function, is_setter=True, is_getter_optional=self.spine_function.returns_optional).write()
+           function_string += "\n"
+           function_string += inset + inset + "}"
+
+        function_string += "\n"
+        function_string += inset + "}"
+        function_string += "\n"
+
+        return function_string
+    
+    def write_computed_property_signature(self, function_name, swift_return_type):
+      property_name = snake_to_camel(function_name.replace("get_", ""))
+      property_string = f"public var {property_name}: {swift_return_type}"
+      if self.spine_function.returns_optional:
+        property_string += "?"
+      return property_string
+
+    def write_method_signature(self, function_name, swift_return_type):
+
+      function_string = ""
+      
+      if not self.spine_function.return_type == "void":
+         function_string += "@discardableResult"
+         function_string += "\n"
+         function_string += inset
+
+      function_string += f"public func {snake_to_camel(function_name)}"
+
+      function_string += "("
+      
+      spine_params = self.spine_function.parameters;
+
+      # Filter out ivar
+      if spine_params and spine_params[0].type == self.spine_object.name:
+        spine_params_without_ivar = spine_params[1:] 
+      else:
+        spine_params_without_ivar = spine_params
+
+      swift_params = [
+          SwiftParamWriter(param = spine_param).write()
+          for spine_param in spine_params_without_ivar
+      ]
+
+      function_string += ", ".join(swift_params)
+      function_string += ")"
+
+      if not self.spine_function.return_type == "void":
+        function_string += f" -> {swift_return_type}"
+
+      if "find_" in function_name or self.spine_function.returns_optional:
+         function_string += "?"
+
+      return function_string
+
+class SwiftObjectWriter:
+    def __init__(self, spine_object):
+        self.spine_object = spine_object
+
+    def write(self):
+        ivar_type = self.spine_object.name
+        ivar_name = self.spine_object.var_name
+
+        class_name = snake_to_title(self.spine_object.name.replace("spine_", ""))
+        
+        object_string = f"@objc(Spine{class_name})"
+        object_string += "\n"
+        object_string += "@objcMembers"
+        object_string += "\n"
+        object_string += f"public final class {class_name}: NSObject"
+        object_string += " {"
+        object_string += "\n"
+        object_string += "\n"
+        object_string += inset
+        object_string += f"internal let {ivar_name}: {ivar_type}"
+        object_string += "\n"
+
+        if any("dispose" in function_name for function_name in self.spine_object.function_names):
+          object_string += inset
+          object_string += f"internal var disposed = false"
+          object_string += "\n"
+
+        object_string += "\n"
+
+        object_string += inset
+        object_string += f"internal init(_ {ivar_name}: {ivar_type})"
+        object_string += " {"
+        object_string += "\n"
+        object_string += inset + inset
+        object_string += f"self.{ivar_name} = {ivar_name}"
+        object_string += "\n"
+        object_string += inset + inset
+        object_string += "super.init()"
+        object_string += "\n"
+        object_string += inset
+        object_string += "}"
+        object_string += "\n"
+        object_string += "\n"
+        
+        filtered_spine_functions = [spine_function for spine_function in self.spine_object.functions if not "_get_num_" in spine_function.name]
+
+        spine_functions_by_name = {}
+        getter_names = []
+        setter_names = []
+        method_names = []
+        
+        for spine_function in filtered_spine_functions:
+          spine_functions_by_name[spine_function.name] = spine_function
+
+          if ("_get_" in spine_function.name or "_is_" in spine_function.name) and len(spine_function.parameters) == 1:
+            getter_names.append(spine_function.name)
+          elif "_set_" in spine_function.name and len(spine_function.parameters) == 2:
+            setter_names.append(spine_function.name)
+          else:
+            method_names.append(spine_function.name)
+
+        get_set_pairs = []
+
+        for setter_name in setter_names:
+          getter_name_get = setter_name.replace("_set_", "_get_")
+          getter_name_is = setter_name.replace("_set_", "_is_")
+          if getter_name_get in getter_names:
+            getter_names.remove(getter_name_get)
+            get_set_pairs.append((getter_name_get, setter_name))
+          elif getter_name_is in getter_names:
+            getter_names.remove(getter_name_is)
+            get_set_pairs.append((getter_name_is, setter_name))
+          else:
+            method_names.append(setter_name) # Coul not find getter by name. Move to methods
+        
+        # print(get_set_pairs)
+
+        for getter_name in getter_names:
+          spine_function = spine_functions_by_name[getter_name]
+          object_string += SwiftFunctionWriter(spine_object = self.spine_object, spine_function = spine_function, spine_setter_function=None).write()
+          object_string += "\n"
+        
+        for get_set_pair in get_set_pairs:
+          getter_function = spine_functions_by_name[get_set_pair[0]]
+          setter_function = spine_functions_by_name[get_set_pair[1]]
+          object_string += SwiftFunctionWriter(spine_object = self.spine_object, spine_function = getter_function, spine_setter_function=setter_function).write()
+          object_string += "\n"
+
+        for method_name in method_names:
+          spine_function = spine_functions_by_name[method_name]
+          object_string += SwiftFunctionWriter(spine_object = self.spine_object, spine_function = spine_function, spine_setter_function=None).write()
+          object_string += "\n"
+
+        object_string += "}"
+
+        return object_string
+
+class SwiftEnumWriter:
+    def __init__(self, spine_enum):
+        self.spine_enum = spine_enum
+
+    def write(self):
+       # TODO: Consider leaving spine prefix (objc) or map whole c enum to swift/objc compatible enum
+       return f"public typealias {snake_to_title(self.spine_enum.replace("spine_", ""))} = {self.spine_enum}"
+
+print("import Foundation")
+print("import SpineCppLite")
+print("")
+
+for enum in enums:
+   print(SwiftEnumWriter(spine_enum=enum).write())
+
+print("")
+  
+for object in objects:
+    print(SwiftObjectWriter(spine_object = object).write())
+    print("")

+ 20 - 4
spine-cpp/spine-cpp-lite/spine-cpp-lite.cpp

@@ -295,6 +295,16 @@ int32_t spine_atlas_get_num_image_paths(spine_atlas atlas) {
 	return ((_spine_atlas *) atlas)->numImagePaths;
 }
 
+spine_bool spine_atlas_is_pma(spine_atlas atlas) {
+	if (!atlas) return 0;
+	Atlas *_atlas = static_cast<Atlas *>(((_spine_atlas *) atlas)->atlas);
+	if (_atlas->getPages().size() > 0) {
+		return _atlas->getPages()[0]->pma;
+	} else {
+		return 0;
+	}
+}
+
 utf8 *spine_atlas_get_image_path(spine_atlas atlas, int32_t index) {
 	if (!atlas) return nullptr;
 	return ((_spine_atlas *) atlas)->imagePaths[index];
@@ -1919,7 +1929,7 @@ void spine_slot_data_set_dark_color(spine_slot_data slot, float r, float g, floa
 	_slot->getDarkColor().set(r, g, b, a);
 }
 
-spine_bool spine_slot_data_has_dark_color(spine_slot_data slot) {
+spine_bool spine_slot_data_get_has_dark_color(spine_slot_data slot) {
 	if (slot == nullptr) return 0;
 	SlotData *_slot = (SlotData *) slot;
 	return _slot->hasDarkColor() ? -1 : 0;
@@ -2173,7 +2183,7 @@ void spine_bone_data_set_inherit(spine_bone_data data, spine_inherit inherit) {
 	_data->setInherit((Inherit) inherit);
 }
 
-spine_bool spine_bone_data_is_skin_required(spine_bone_data data) {
+spine_bool spine_bone_data_get_is_skin_required(spine_bone_data data) {
 	if (data == nullptr) return 0;
 	BoneData *_data = (BoneData *) data;
 	return _data->isSkinRequired() ? -1 : 0;
@@ -2503,7 +2513,7 @@ float spine_bone_get_a_shear_y(spine_bone bone) {
 	return _bone->getAShearY();
 }
 
-void spine_bone_set_shear_a_y(spine_bone bone, float shearY) {
+void spine_bone_set_a_shear_y(spine_bone bone, float shearY) {
 	if (bone == nullptr) return;
 	Bone *_bone = (Bone *) bone;
 	_bone->setAShearY(shearY);
@@ -3389,6 +3399,12 @@ spine_bool spine_ik_constraint_data_get_uniform(spine_ik_constraint_data data) {
 	return _data->getUniform() ? -1 : 0;
 }
 
+void spine_ik_constraint_data_set_uniform(spine_ik_constraint_data data, spine_bool uniform) {
+	if (data == nullptr) return;
+	IkConstraintData *_data = (IkConstraintData *) data;
+	_data->setUniform(uniform);
+}
+
 float spine_ik_constraint_data_get_mix(spine_ik_constraint_data data) {
 	if (data == nullptr) return 0;
 	IkConstraintData *_data = (IkConstraintData *) data;
@@ -4674,7 +4690,7 @@ float spine_physics_constraint_get_last_time(spine_physics_constraint constraint
 	return _constraint->getLastTime();
 }
 
-void spine_physics_constraint_reset(spine_physics_constraint constraint) {
+void spine_physics_constraint_reset_fully(spine_physics_constraint constraint) {
 	if (constraint == nullptr) return;
 	PhysicsConstraint *_constraint = (PhysicsConstraint *) constraint;
 	_constraint->reset();

+ 53 - 4
spine-cpp/spine-cpp-lite/spine-cpp-lite.h

@@ -59,6 +59,8 @@
 	} name##_wrapper;               \
 	typedef name##_wrapper *name;
 
+// @start: opaque_types
+
 SPINE_OPAQUE_TYPE(spine_skeleton)
 SPINE_OPAQUE_TYPE(spine_skeleton_data)
 SPINE_OPAQUE_TYPE(spine_bone)
@@ -103,8 +105,12 @@ SPINE_OPAQUE_TYPE(spine_skeleton_drawable)
 SPINE_OPAQUE_TYPE(spine_skin_entry)
 SPINE_OPAQUE_TYPE(spine_skin_entries)
 
+// @end: opaque_types
+
 typedef char utf8;
 
+// @start: enums
+
 typedef enum spine_blend_mode {
 	SPINE_BLEND_MODE_NORMAL = 0,
 	SPINE_BLEND_MODE_ADDITIVE,
@@ -177,8 +183,12 @@ typedef enum spine_physics {
 
 } spine_physics;
 
+// @end: enums
+
 typedef int32_t spine_bool;
 
+// @start: function_declarations
+
 SPINE_CPP_LITE_EXPORT int32_t spine_major_version();
 SPINE_CPP_LITE_EXPORT int32_t spine_minor_version();
 SPINE_CPP_LITE_EXPORT void spine_enable_debug_extension(spine_bool enable);
@@ -200,10 +210,13 @@ SPINE_CPP_LITE_EXPORT float spine_vector_get_y(spine_vector vector);
 SPINE_CPP_LITE_EXPORT spine_atlas spine_atlas_load(const utf8 *atlasData);
 SPINE_CPP_LITE_EXPORT int32_t spine_atlas_get_num_image_paths(spine_atlas atlas);
 SPINE_CPP_LITE_EXPORT utf8 *spine_atlas_get_image_path(spine_atlas atlas, int32_t index);
+SPINE_CPP_LITE_EXPORT spine_bool spine_atlas_is_pma(spine_atlas atlas);
 SPINE_CPP_LITE_EXPORT utf8 *spine_atlas_get_error(spine_atlas atlas);
 SPINE_CPP_LITE_EXPORT void spine_atlas_dispose(spine_atlas atlas);
 
+// @ignore
 SPINE_CPP_LITE_EXPORT spine_skeleton_data_result spine_skeleton_data_load_json(spine_atlas atlas, const utf8 *skeletonData);
+// @ignore
 SPINE_CPP_LITE_EXPORT spine_skeleton_data_result spine_skeleton_data_load_binary(spine_atlas atlas, const uint8_t *skeletonData, int32_t length);
 SPINE_CPP_LITE_EXPORT utf8 *spine_skeleton_data_result_get_error(spine_skeleton_data_result result);
 SPINE_CPP_LITE_EXPORT spine_skeleton_data spine_skeleton_data_result_get_data(spine_skeleton_data_result result);
@@ -217,6 +230,7 @@ SPINE_CPP_LITE_EXPORT spine_ik_constraint_data spine_skeleton_data_find_ik_const
 SPINE_CPP_LITE_EXPORT spine_transform_constraint_data spine_skeleton_data_find_transform_constraint(spine_skeleton_data data, const utf8 *name);
 SPINE_CPP_LITE_EXPORT spine_path_constraint_data spine_skeleton_data_find_path_constraint(spine_skeleton_data data, const utf8 *name);
 SPINE_CPP_LITE_EXPORT spine_physics_constraint_data spine_skeleton_data_find_physics_constraint(spine_skeleton_data data, const utf8 *name);
+// @optiona;
 SPINE_CPP_LITE_EXPORT const utf8 *spine_skeleton_data_get_name(spine_skeleton_data data);
 // OMITTED setName()
 SPINE_CPP_LITE_EXPORT int32_t spine_skeleton_data_get_num_bones(spine_skeleton_data data);
@@ -225,6 +239,7 @@ SPINE_CPP_LITE_EXPORT int32_t spine_skeleton_data_get_num_slots(spine_skeleton_d
 SPINE_CPP_LITE_EXPORT spine_slot_data *spine_skeleton_data_get_slots(spine_skeleton_data data);
 SPINE_CPP_LITE_EXPORT int32_t spine_skeleton_data_get_num_skins(spine_skeleton_data data);
 SPINE_CPP_LITE_EXPORT spine_skin *spine_skeleton_data_get_skins(spine_skeleton_data data);
+// @optional
 SPINE_CPP_LITE_EXPORT spine_skin spine_skeleton_data_get_default_skin(spine_skeleton_data data);
 SPINE_CPP_LITE_EXPORT void spine_skeleton_data_set_default_skin(spine_skeleton_data data, spine_skin skin);
 SPINE_CPP_LITE_EXPORT int32_t spine_skeleton_data_get_num_events(spine_skeleton_data data);
@@ -248,7 +263,8 @@ SPINE_CPP_LITE_EXPORT void spine_skeleton_data_set_width(spine_skeleton_data dat
 SPINE_CPP_LITE_EXPORT float spine_skeleton_data_get_height(spine_skeleton_data data);
 SPINE_CPP_LITE_EXPORT void spine_skeleton_data_set_height(spine_skeleton_data data, float height);
 SPINE_CPP_LITE_EXPORT const utf8 *spine_skeleton_data_get_version(spine_skeleton_data data);
-// OMITTED setVersion()
+// OMITTED setVersion() 
+// @ignore
 SPINE_CPP_LITE_EXPORT const utf8 *spine_skeleton_data_get_hash(spine_skeleton_data data);
 // OMITTED setHash()
 SPINE_CPP_LITE_EXPORT const utf8 *spine_skeleton_data_get_images_path(spine_skeleton_data data);
@@ -260,7 +276,9 @@ SPINE_CPP_LITE_EXPORT float spine_skeleton_data_get_fps(spine_skeleton_data data
 SPINE_CPP_LITE_EXPORT float spine_skeleton_data_get_reference_scale(spine_skeleton_data data);
 SPINE_CPP_LITE_EXPORT void spine_skeleton_data_dispose(spine_skeleton_data data);
 
+// @ignore
 SPINE_CPP_LITE_EXPORT spine_skeleton_drawable spine_skeleton_drawable_create(spine_skeleton_data skeletonData);
+// @ignore
 SPINE_CPP_LITE_EXPORT spine_render_command spine_skeleton_drawable_render(spine_skeleton_drawable drawable);
 SPINE_CPP_LITE_EXPORT void spine_skeleton_drawable_dispose(spine_skeleton_drawable drawable);
 SPINE_CPP_LITE_EXPORT spine_skeleton spine_skeleton_drawable_get_skeleton(spine_skeleton_drawable drawable);
@@ -268,8 +286,11 @@ SPINE_CPP_LITE_EXPORT spine_animation_state spine_skeleton_drawable_get_animatio
 SPINE_CPP_LITE_EXPORT spine_animation_state_data spine_skeleton_drawable_get_animation_state_data(spine_skeleton_drawable drawable);
 SPINE_CPP_LITE_EXPORT spine_animation_state_events spine_skeleton_drawable_get_animation_state_events(spine_skeleton_drawable drawable);
 
+// @ignore
 SPINE_CPP_LITE_EXPORT float *spine_render_command_get_positions(spine_render_command command);
+// @ignore
 SPINE_CPP_LITE_EXPORT float *spine_render_command_get_uvs(spine_render_command command);
+// @ignore
 SPINE_CPP_LITE_EXPORT int32_t *spine_render_command_get_colors(spine_render_command command);
 SPINE_CPP_LITE_EXPORT int32_t spine_render_command_get_num_vertices(spine_render_command command);
 SPINE_CPP_LITE_EXPORT uint16_t *spine_render_command_get_indices(spine_render_command command);
@@ -305,6 +326,7 @@ SPINE_CPP_LITE_EXPORT spine_track_entry spine_animation_state_add_animation(spin
 SPINE_CPP_LITE_EXPORT spine_track_entry spine_animation_state_set_empty_animation(spine_animation_state state, int32_t trackIndex, float mixDuration);
 SPINE_CPP_LITE_EXPORT spine_track_entry spine_animation_state_add_empty_animation(spine_animation_state state, int32_t trackIndex, float mixDuration, float delay);
 SPINE_CPP_LITE_EXPORT void spine_animation_state_set_empty_animations(spine_animation_state state, float mixDuration);
+// @optional
 SPINE_CPP_LITE_EXPORT spine_track_entry spine_animation_state_get_current(spine_animation_state state, int32_t trackIndex);
 SPINE_CPP_LITE_EXPORT spine_animation_state_data spine_animation_state_get_data(spine_animation_state state);
 SPINE_CPP_LITE_EXPORT float spine_animation_state_get_time_scale(spine_animation_state state);
@@ -315,11 +337,13 @@ SPINE_CPP_LITE_EXPORT void spine_animation_state_set_time_scale(spine_animation_
 // OMITTED enableQueue()
 // OMITTED setManualTrackEntryDisposal()
 // OMITTED getManualTrackEntryDisposal()
+// @ignore
 SPINE_CPP_LITE_EXPORT void spine_animation_state_dispose_track_entry(spine_animation_state state, spine_track_entry entry);
 
 SPINE_CPP_LITE_EXPORT int32_t spine_animation_state_events_get_num_events(spine_animation_state_events events);
 SPINE_CPP_LITE_EXPORT spine_event_type spine_animation_state_events_get_event_type(spine_animation_state_events events, int32_t index);
 SPINE_CPP_LITE_EXPORT spine_track_entry spine_animation_state_events_get_track_entry(spine_animation_state_events events, int32_t index);
+// @optional
 SPINE_CPP_LITE_EXPORT spine_event spine_animation_state_events_get_event(spine_animation_state_events events, int32_t index);
 SPINE_CPP_LITE_EXPORT void spine_animation_state_events_reset(spine_animation_state_events events);
 
@@ -359,6 +383,7 @@ SPINE_CPP_LITE_EXPORT float spine_track_entry_get_mix_attachment_threshold(spine
 SPINE_CPP_LITE_EXPORT void spine_track_entry_set_mix_attachment_threshold(spine_track_entry entry, float attachmentThreshold);
 SPINE_CPP_LITE_EXPORT float spine_track_entry_get_mix_draw_order_threshold(spine_track_entry entry);
 SPINE_CPP_LITE_EXPORT void spine_track_entry_set_mix_draw_order_threshold(spine_track_entry entry, float drawOrderThreshold);
+// @optional
 SPINE_CPP_LITE_EXPORT spine_track_entry spine_track_entry_get_next(spine_track_entry entry);
 SPINE_CPP_LITE_EXPORT spine_bool spine_track_entry_is_complete(spine_track_entry entry);
 SPINE_CPP_LITE_EXPORT float spine_track_entry_get_mix_time(spine_track_entry entry);
@@ -367,7 +392,9 @@ SPINE_CPP_LITE_EXPORT float spine_track_entry_get_mix_duration(spine_track_entry
 SPINE_CPP_LITE_EXPORT void spine_track_entry_set_mix_duration(spine_track_entry entry, float mixDuration);
 SPINE_CPP_LITE_EXPORT spine_mix_blend spine_track_entry_get_mix_blend(spine_track_entry entry);
 SPINE_CPP_LITE_EXPORT void spine_track_entry_set_mix_blend(spine_track_entry entry, spine_mix_blend mixBlend);
+// @optional
 SPINE_CPP_LITE_EXPORT spine_track_entry spine_track_entry_get_mixing_from(spine_track_entry entry);
+// @optional
 SPINE_CPP_LITE_EXPORT spine_track_entry spine_track_entry_get_mixing_to(spine_track_entry entry);
 SPINE_CPP_LITE_EXPORT void spine_track_entry_reset_rotation_directions(spine_track_entry entry);
 SPINE_CPP_LITE_EXPORT float spine_track_entry_get_track_complete(spine_track_entry entry);
@@ -387,7 +414,9 @@ SPINE_CPP_LITE_EXPORT spine_bone spine_skeleton_find_bone(spine_skeleton skeleto
 SPINE_CPP_LITE_EXPORT spine_slot spine_skeleton_find_slot(spine_skeleton skeleton, const utf8 *slotName);
 SPINE_CPP_LITE_EXPORT void spine_skeleton_set_skin_by_name(spine_skeleton skeleton, const utf8 *skinName);
 SPINE_CPP_LITE_EXPORT void spine_skeleton_set_skin(spine_skeleton skeleton, spine_skin skin);
+// @optional
 SPINE_CPP_LITE_EXPORT spine_attachment spine_skeleton_get_attachment_by_name(spine_skeleton skeleton, const utf8 *slotName, const utf8 *attachmentName);
+// @optional
 SPINE_CPP_LITE_EXPORT spine_attachment spine_skeleton_get_attachment(spine_skeleton skeleton, int32_t slotIndex, const utf8 *attachmentName);
 SPINE_CPP_LITE_EXPORT void spine_skeleton_set_attachment(spine_skeleton skeleton, const utf8 *slotName, const utf8 *attachmentName);
 SPINE_CPP_LITE_EXPORT spine_ik_constraint spine_skeleton_find_ik_constraint(spine_skeleton skeleton, const utf8 *constraintName);
@@ -395,7 +424,9 @@ SPINE_CPP_LITE_EXPORT spine_transform_constraint spine_skeleton_find_transform_c
 SPINE_CPP_LITE_EXPORT spine_path_constraint spine_skeleton_find_path_constraint(spine_skeleton skeleton, const utf8 *constraintName);
 SPINE_CPP_LITE_EXPORT spine_physics_constraint spine_skeleton_find_physics_constraint(spine_skeleton skeleton, const utf8 *constraintName);
 SPINE_CPP_LITE_EXPORT spine_bounds spine_skeleton_get_bounds(spine_skeleton skeleton);
+// @optional
 SPINE_CPP_LITE_EXPORT spine_bone spine_skeleton_get_root_bone(spine_skeleton skeleton);
+// @optional
 SPINE_CPP_LITE_EXPORT spine_skeleton_data spine_skeleton_get_data(spine_skeleton skeleton);
 SPINE_CPP_LITE_EXPORT int32_t spine_skeleton_get_num_bones(spine_skeleton skeleton);
 SPINE_CPP_LITE_EXPORT spine_bone *spine_skeleton_get_bones(spine_skeleton skeleton);
@@ -412,6 +443,7 @@ SPINE_CPP_LITE_EXPORT int32_t spine_skeleton_get_num_path_constraints(spine_skel
 SPINE_CPP_LITE_EXPORT spine_path_constraint *spine_skeleton_get_path_constraints(spine_skeleton skeleton);
 SPINE_CPP_LITE_EXPORT int32_t spine_skeleton_get_num_physics_constraints(spine_skeleton skeleton);
 SPINE_CPP_LITE_EXPORT spine_physics_constraint *spine_skeleton_get_physics_constraints(spine_skeleton skeleton);
+// @optional
 SPINE_CPP_LITE_EXPORT spine_skin spine_skeleton_get_skin(spine_skeleton skeleton);
 SPINE_CPP_LITE_EXPORT spine_color spine_skeleton_get_color(spine_skeleton skeleton);
 SPINE_CPP_LITE_EXPORT void spine_skeleton_set_color(spine_skeleton skeleton, float r, float g, float b, float a);
@@ -462,7 +494,7 @@ SPINE_CPP_LITE_EXPORT spine_color spine_slot_data_get_color(spine_slot_data slot
 SPINE_CPP_LITE_EXPORT void spine_slot_data_set_color(spine_slot_data slot, float r, float g, float b, float a);
 SPINE_CPP_LITE_EXPORT spine_color spine_slot_data_get_dark_color(spine_slot_data slot);
 SPINE_CPP_LITE_EXPORT void spine_slot_data_set_dark_color(spine_slot_data slot, float r, float g, float b, float a);
-SPINE_CPP_LITE_EXPORT spine_bool spine_slot_data_has_dark_color(spine_slot_data slot);
+SPINE_CPP_LITE_EXPORT spine_bool spine_slot_data_get_has_dark_color(spine_slot_data slot);
 SPINE_CPP_LITE_EXPORT void spine_slot_data_set_has_dark_color(spine_slot_data slot, spine_bool hasDarkColor);
 SPINE_CPP_LITE_EXPORT const utf8 *spine_slot_data_get_attachment_name(spine_slot_data slot);
 SPINE_CPP_LITE_EXPORT void spine_slot_data_set_attachment_name(spine_slot_data slot, const utf8 *attachmentName);
@@ -481,6 +513,7 @@ SPINE_CPP_LITE_EXPORT void spine_slot_set_color(spine_slot slot, float r, float
 SPINE_CPP_LITE_EXPORT spine_color spine_slot_get_dark_color(spine_slot slot);
 SPINE_CPP_LITE_EXPORT void spine_slot_set_dark_color(spine_slot slot, float r, float g, float b, float a);
 SPINE_CPP_LITE_EXPORT spine_bool spine_slot_has_dark_color(spine_slot slot);
+// @optional
 SPINE_CPP_LITE_EXPORT spine_attachment spine_slot_get_attachment(spine_slot slot);
 SPINE_CPP_LITE_EXPORT void spine_slot_set_attachment(spine_slot slot, spine_attachment attachment);
 // OMITTED getDeform()
@@ -489,6 +522,7 @@ SPINE_CPP_LITE_EXPORT void spine_slot_set_sequence_index(spine_slot slot, int32_
 
 SPINE_CPP_LITE_EXPORT int32_t spine_bone_data_get_index(spine_bone_data data);
 SPINE_CPP_LITE_EXPORT const utf8 *spine_bone_data_get_name(spine_bone_data data);
+// @optional
 SPINE_CPP_LITE_EXPORT spine_bone_data spine_bone_data_get_parent(spine_bone_data data);
 SPINE_CPP_LITE_EXPORT float spine_bone_data_get_length(spine_bone_data data);
 SPINE_CPP_LITE_EXPORT void spine_bone_data_set_length(spine_bone_data data, float length);
@@ -508,7 +542,7 @@ SPINE_CPP_LITE_EXPORT float spine_bone_data_get_shear_y(spine_bone_data data);
 SPINE_CPP_LITE_EXPORT void spine_bone_data_set_shear_y(spine_bone_data data, float shearY);
 SPINE_CPP_LITE_EXPORT spine_inherit spine_bone_data_get_inherit(spine_bone_data data);
 SPINE_CPP_LITE_EXPORT void spine_bone_data_set_inherit(spine_bone_data data, spine_inherit inherit);
-SPINE_CPP_LITE_EXPORT spine_bool spine_bone_data_is_skin_required(spine_bone_data data);
+SPINE_CPP_LITE_EXPORT spine_bool spine_bone_data_get_is_skin_required(spine_bone_data data);
 SPINE_CPP_LITE_EXPORT void spine_bone_data_set_is_skin_required(spine_bone_data data, spine_bool isSkinRequired);
 SPINE_CPP_LITE_EXPORT spine_color spine_bone_data_get_color(spine_bone_data data);
 SPINE_CPP_LITE_EXPORT void spine_bone_data_set_color(spine_bone_data data, float r, float g, float b, float a);
@@ -534,6 +568,7 @@ SPINE_CPP_LITE_EXPORT float spine_bone_get_world_to_local_rotation_x(spine_bone
 SPINE_CPP_LITE_EXPORT float spine_bone_get_world_to_local_rotation_y(spine_bone bone);
 SPINE_CPP_LITE_EXPORT spine_bone_data spine_bone_get_data(spine_bone bone);
 SPINE_CPP_LITE_EXPORT spine_skeleton spine_bone_get_skeleton(spine_bone bone);
+// @optional
 SPINE_CPP_LITE_EXPORT spine_bone spine_bone_get_parent(spine_bone bone);
 SPINE_CPP_LITE_EXPORT int32_t spine_bone_get_num_children(spine_bone bone);
 SPINE_CPP_LITE_EXPORT spine_bone *spine_bone_get_children(spine_bone bone);
@@ -588,6 +623,7 @@ SPINE_CPP_LITE_EXPORT void spine_bone_set_inherit(spine_bone data, spine_inherit
 
 SPINE_CPP_LITE_EXPORT const utf8 *spine_attachment_get_name(spine_attachment attachment);
 SPINE_CPP_LITE_EXPORT spine_attachment_type spine_attachment_get_type(spine_attachment attachment);
+// @ignore
 SPINE_CPP_LITE_EXPORT spine_attachment spine_attachment_copy(spine_attachment attachment);
 SPINE_CPP_LITE_EXPORT void spine_attachment_dispose(spine_attachment attachment);
 
@@ -603,6 +639,7 @@ SPINE_CPP_LITE_EXPORT spine_color spine_point_attachment_get_color(spine_point_a
 SPINE_CPP_LITE_EXPORT void spine_point_attachment_set_color(spine_point_attachment attachment, float r, float g, float b, float a);
 
 SPINE_CPP_LITE_EXPORT void spine_region_attachment_update_region(spine_region_attachment attachment);
+// @ignore
 SPINE_CPP_LITE_EXPORT void spine_region_attachment_compute_world_vertices(spine_region_attachment attachment, spine_slot slot, float *worldVertices);
 SPINE_CPP_LITE_EXPORT float spine_region_attachment_get_x(spine_region_attachment attachment);
 SPINE_CPP_LITE_EXPORT void spine_region_attachment_set_x(spine_region_attachment attachment, float x);
@@ -622,8 +659,10 @@ SPINE_CPP_LITE_EXPORT spine_color spine_region_attachment_get_color(spine_region
 SPINE_CPP_LITE_EXPORT void spine_region_attachment_set_color(spine_region_attachment attachment, float r, float g, float b, float a);
 SPINE_CPP_LITE_EXPORT const utf8 *spine_region_attachment_get_path(spine_region_attachment attachment);
 // OMITTED setPath()
+// @optional
 SPINE_CPP_LITE_EXPORT spine_texture_region spine_region_attachment_get_region(spine_region_attachment attachment);
 // OMITTED setRegion()
+// @optional
 SPINE_CPP_LITE_EXPORT spine_sequence spine_region_attachment_get_sequence(spine_region_attachment attachment);
 // OMITTED setSequence()
 SPINE_CPP_LITE_EXPORT int32_t spine_region_attachment_get_num_offset(spine_region_attachment attachment);
@@ -632,12 +671,14 @@ SPINE_CPP_LITE_EXPORT int32_t spine_region_attachment_get_num_uvs(spine_region_a
 SPINE_CPP_LITE_EXPORT float *spine_region_attachment_get_uvs(spine_region_attachment attachment);
 
 SPINE_CPP_LITE_EXPORT int32_t spine_vertex_attachment_get_world_vertices_length(spine_vertex_attachment attachment);
+// @ignore
 SPINE_CPP_LITE_EXPORT void spine_vertex_attachment_compute_world_vertices(spine_vertex_attachment attachment, spine_slot slot, float *worldVertices);
 // OMITTED getId()
 SPINE_CPP_LITE_EXPORT int32_t spine_vertex_attachment_get_num_bones(spine_vertex_attachment attachment);
 SPINE_CPP_LITE_EXPORT int32_t *spine_vertex_attachment_get_bones(spine_vertex_attachment attachment);
 SPINE_CPP_LITE_EXPORT int32_t spine_vertex_attachment_get_num_vertices(spine_vertex_attachment attachment);
 SPINE_CPP_LITE_EXPORT float *spine_vertex_attachment_get_vertices(spine_vertex_attachment attachment);
+// @optional
 SPINE_CPP_LITE_EXPORT spine_attachment spine_vertex_attachment_get_timeline_attachment(spine_vertex_attachment timelineAttachment);
 SPINE_CPP_LITE_EXPORT void spine_vertex_attachment_set_timeline_attachment(spine_vertex_attachment attachment, spine_attachment timelineAttachment);
 // OMITTED copyTo()
@@ -657,8 +698,10 @@ SPINE_CPP_LITE_EXPORT const utf8 *spine_mesh_attachment_get_path(spine_mesh_atta
 // OMITTED setPath()
 SPINE_CPP_LITE_EXPORT spine_texture_region spine_mesh_attachment_get_region(spine_mesh_attachment attachment);
 // OMITTED setRegion()
+// @optional
 SPINE_CPP_LITE_EXPORT spine_sequence spine_mesh_attachment_get_sequence(spine_mesh_attachment attachment);
 // OMITTED setSequence()
+// @optional
 SPINE_CPP_LITE_EXPORT spine_mesh_attachment spine_mesh_attachment_get_parent_mesh(spine_mesh_attachment attachment);
 SPINE_CPP_LITE_EXPORT void spine_mesh_attachment_set_parent_mesh(spine_mesh_attachment attachment, spine_mesh_attachment parentMesh);
 SPINE_CPP_LITE_EXPORT int32_t spine_mesh_attachment_get_num_edges(spine_mesh_attachment attachment);
@@ -669,6 +712,7 @@ SPINE_CPP_LITE_EXPORT float spine_mesh_attachment_get_height(spine_mesh_attachme
 SPINE_CPP_LITE_EXPORT void spine_mesh_attachment_set_height(spine_mesh_attachment attachment, float height);
 // OMITTED newLinkedMesh()
 
+// @optional
 SPINE_CPP_LITE_EXPORT spine_slot_data spine_clipping_attachment_get_end_slot(spine_clipping_attachment attachment);
 SPINE_CPP_LITE_EXPORT void spine_clipping_attachment_set_end_slot(spine_clipping_attachment attachment, spine_slot_data endSlot);
 SPINE_CPP_LITE_EXPORT spine_color spine_clipping_attachment_get_color(spine_clipping_attachment attachment);
@@ -687,6 +731,7 @@ SPINE_CPP_LITE_EXPORT spine_color spine_path_attachment_get_color(spine_path_att
 SPINE_CPP_LITE_EXPORT void spine_path_attachment_set_color(spine_path_attachment attachment, float r, float g, float b, float a);
 
 SPINE_CPP_LITE_EXPORT void spine_skin_set_attachment(spine_skin skin, int32_t slotIndex, const utf8 *name, spine_attachment attachment);
+// @optional
 SPINE_CPP_LITE_EXPORT spine_attachment spine_skin_get_attachment(spine_skin skin, int32_t slotIndex, const utf8 *name);
 SPINE_CPP_LITE_EXPORT void spine_skin_remove_attachment(spine_skin skin, int32_t slotIndex, const utf8 *name);
 // OMITTED findNamesForSlot()
@@ -706,6 +751,7 @@ SPINE_CPP_LITE_EXPORT int32_t spine_skin_get_num_bones(spine_skin skin);
 SPINE_CPP_LITE_EXPORT spine_bone_data *spine_skin_get_bones(spine_skin skin);
 SPINE_CPP_LITE_EXPORT int32_t spine_skin_get_num_constraints(spine_skin skin);
 SPINE_CPP_LITE_EXPORT spine_constraint_data *spine_skin_get_constraints(spine_skin skin);
+// @ignore
 SPINE_CPP_LITE_EXPORT spine_skin spine_skin_create(const utf8 *name);
 SPINE_CPP_LITE_EXPORT void spine_skin_dispose(spine_skin skin);
 
@@ -808,6 +854,7 @@ SPINE_CPP_LITE_EXPORT float spine_transform_constraint_get_mix_shear_y(spine_tra
 SPINE_CPP_LITE_EXPORT void spine_transform_constraint_set_mix_shear_y(spine_transform_constraint constraint, float mixShearY);
 SPINE_CPP_LITE_EXPORT spine_bool spine_transform_constraint_get_is_active(spine_transform_constraint constraint);
 SPINE_CPP_LITE_EXPORT void spine_transform_constraint_set_is_active(spine_transform_constraint constraint, spine_bool isActive);
+
 // OMITTED setToSetupPose()
 
 SPINE_CPP_LITE_EXPORT int32_t spine_path_constraint_data_get_num_bones(spine_path_constraint_data data);
@@ -951,7 +998,7 @@ SPINE_CPP_LITE_EXPORT void spine_physics_constraint_set_remaining(spine_physics_
 SPINE_CPP_LITE_EXPORT float spine_physics_constraint_get_remaining(spine_physics_constraint constraint);
 SPINE_CPP_LITE_EXPORT void spine_physics_constraint_set_last_time(spine_physics_constraint constraint, float value);
 SPINE_CPP_LITE_EXPORT float spine_physics_constraint_get_last_time(spine_physics_constraint constraint);
-SPINE_CPP_LITE_EXPORT void spine_physics_constraint_reset(spine_physics_constraint constraint);
+SPINE_CPP_LITE_EXPORT void spine_physics_constraint_reset_fully(spine_physics_constraint constraint);
 // Omitted setToSetupPose()
 SPINE_CPP_LITE_EXPORT void spine_physics_constraint_update(spine_physics_constraint data, spine_physics physics);
 SPINE_CPP_LITE_EXPORT void spine_physics_constraint_translate(spine_physics_constraint data, float x, float y);
@@ -997,4 +1044,6 @@ SPINE_CPP_LITE_EXPORT void spine_texture_region_set_original_width(spine_texture
 SPINE_CPP_LITE_EXPORT int32_t spine_texture_region_get_original_height(spine_texture_region textureRegion);
 SPINE_CPP_LITE_EXPORT void spine_texture_region_set_original_height(spine_texture_region textureRegion, int32_t originalHeight);
 
+// @end: function_declarations
+
 #endif

+ 2 - 2
spine-flutter/example/lib/animation_state_events.dart

@@ -23,14 +23,14 @@ class AnimationStateEvents extends StatelessWidget {
       controller.animationState.setListener((type, trackEntry, event) {
         if (type == EventType.event) {
           print(
-              "User event: { name: ${event?.getData().getName()}, intValue: ${event?.getIntValue()}, floatValue: intValue: ${event?.getFloatValue()}, stringValue: ${event?.getStringValue()} }");
+              "User event: { name: ${event?.getData().getName()}, intValue: ${event?.getIntValue()}, floatValue: ${event?.getFloatValue()}, stringValue: ${event?.getStringValue()} }");
         }
       });
       print("Current: ${controller.animationState.getCurrent(0)?.getAnimation().getName()}");
     });
 
     return Scaffold(
-        appBar: AppBar(title: const Text('Spineboy')),
+        appBar: AppBar(title: const Text('Animation State Listener')),
         body: Column(children: [
           const Text("See output in console!"),
           Expanded(child: SpineWidget.fromAsset("assets/spineboy.atlas", "assets/spineboy-pro.skel", controller))

+ 0 - 1
spine-flutter/example/lib/physics.dart

@@ -41,7 +41,6 @@ class PhysicsState extends State<PhysicsTest> {
   late SpineWidgetController controller;
   Offset? mousePosition;
   Offset? lastMousePosition;
-  Offset? delta;
 
   @override
   void initState() {

+ 1 - 1
spine-flutter/lib/spine_widget.dart

@@ -659,7 +659,7 @@ class _SpineRenderObject extends RenderBox {
       ..translate(offsetX, offsetY)
       ..scale(scaleX, scaleY)
       ..translate(x, y);
-    _controller._setCoordinateTransform(x + offsetX / scaleY, y + offsetY / scaleY, scaleX, scaleY);
+    _controller._setCoordinateTransform(x + offsetX / scaleX, y + offsetY / scaleY, scaleX, scaleY);
   }
 
   @override

+ 1 - 1
spine-flutter/pubspec.yaml

@@ -45,4 +45,4 @@ flutter:
     - lib/assets/libspine_flutter.js
     - lib/assets/libspine_flutter.wasm
     - src/spine-cpp-lite/spine-cpp-lite.cpp
-    - src/spine-cpp-lite/spine-cpp-lite.h
+    - ../spine-cpp/spine-cpp-lite/spine-cpp-lite.h

+ 8 - 0
spine-ios/.gitignore

@@ -0,0 +1,8 @@
+.DS_Store
+/.build
+/Packages
+xcuserdata/
+DerivedData/
+.swiftpm/configuration/registries.json
+.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
+.netrc

+ 24 - 0
spine-ios/CHANGELOG.md

@@ -0,0 +1,24 @@
+# Changelog
+
+# Unreleased
+
+- Add `spine-iOS` SPM package & example app (#1)
+- Basic Mesh Rendering (#2)
+- Spine C++ Swift Wrapper (#3)
+- Load `Atlas` & `SkeletonData` (#4)
+- Generate Swift classes from `spine-cpp-lite.h` (#5)
+- Draw `SkeletonData` render commands (#6)
+- Add `BoundsProvider` (#7)
+- Support `SpineController` & `Event` callbacks (#8)
+- Support `DressUp` sample (#9)
+- Add `Physics` sample (#10)
+- Add `DebugRendering` sample (#11)
+- Move remaining files to SPM package (#12)
+- Load assets from different sources (#13)
+- Add `Obj-C` + `UIKit` sample (#14)
+- Support CocoaPods (#15)
+- Metal Best Practices (#16)
+- Annotate functions that should return optional (#17)
+- Add option to disable drawing when out of viewport (#18)
+- Add docs for public Spine classes/methods (#19)
+- Fix various regressions (#20)

+ 92 - 0
spine-ios/Example - Cocoapods/.gitignore

@@ -0,0 +1,92 @@
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## User settings
+xcuserdata/
+
+## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
+*.xcscmblueprint
+*.xccheckout
+
+## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
+build/
+DerivedData/
+*.moved-aside
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+
+## Obj-C/Swift specific
+*.hmap
+
+## App packaging
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+## Playgrounds
+timeline.xctimeline
+playground.xcworkspace
+
+# Swift Package Manager
+#
+# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
+# Packages/
+# Package.pins
+# Package.resolved
+# *.xcodeproj
+#
+# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
+# hence it is not needed unless you have added a package configuration file to your project
+# .swiftpm
+
+.build/
+
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+Pods/
+Podfile.lock
+#
+# Add this line if you want to avoid checking in source code from the Xcode workspace
+*.xcworkspace
+
+# Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+Carthage/Build/
+
+# Accio dependency management
+Dependencies/
+.accio/
+
+# fastlane
+#
+# It is recommended to not store the screenshots in the git repo.
+# Instead, use fastlane to re-generate the screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
+
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots/**/*.png
+fastlane/test_output
+
+# Code Injection
+#
+# After new code Injection tools there's a generated folder /iOSInjectionProject
+# https://github.com/johnno1962/injectionforxcode
+
+iOSInjectionProject/
+.DS_store

+ 11 - 0
spine-ios/Example - Cocoapods/Podfile

@@ -0,0 +1,11 @@
+# Uncomment the next line to define a global platform for your project
+platform :ios, '13.0'
+
+target 'Spine iOS Example' do
+  # Comment the next line if you don't want to use dynamic frameworks
+  use_frameworks!
+
+  pod 'Spine', :podspec => 'https://raw.githubusercontent.com/denrase/spine-runtimes/cocoapods/Spine.podspec'
+  pod 'SpineCppLite', :podspec => 'https://raw.githubusercontent.com/denrase/spine-runtimes/cocoapods/SpineCppLite.podspec'
+  pod 'SpineShadersStructs', :podspec => 'https://raw.githubusercontent.com/denrase/spine-runtimes/cocoapods/SpineShadersStructs.podspec'
+end

+ 455 - 0
spine-ios/Example - Cocoapods/Spine iOS Example.xcodeproj/project.pbxproj

@@ -0,0 +1,455 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 56;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		029E40AACC629A2B19C75855 /* Pods_Spine_iOS_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63A23DBBF6B9A1877F1E82A7 /* Pods_Spine_iOS_Example.framework */; };
+		9281EF152C07885C00BE19F5 /* Spine_iOS_ExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9281EF142C07885C00BE19F5 /* Spine_iOS_ExampleApp.swift */; };
+		9281EF192C07885D00BE19F5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9281EF182C07885D00BE19F5 /* Assets.xcassets */; };
+		9281EF1C2C07885D00BE19F5 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9281EF1B2C07885D00BE19F5 /* Preview Assets.xcassets */; };
+		92BFC4B62C11BCA10051F2EA /* spineboy-pro.skel in Resources */ = {isa = PBXBuildFile; fileRef = 92BFC4B22C11BCA10051F2EA /* spineboy-pro.skel */; };
+		92BFC4B72C11BCA10051F2EA /* spineboy-pro.json in Resources */ = {isa = PBXBuildFile; fileRef = 92BFC4B32C11BCA10051F2EA /* spineboy-pro.json */; };
+		92BFC4B82C11BCA10051F2EA /* spineboy.png in Resources */ = {isa = PBXBuildFile; fileRef = 92BFC4B42C11BCA10051F2EA /* spineboy.png */; };
+		92BFC4B92C11BCA10051F2EA /* spineboy.atlas in Resources */ = {isa = PBXBuildFile; fileRef = 92BFC4B52C11BCA10051F2EA /* spineboy.atlas */; };
+		92BFC4BB2C11BCC50051F2EA /* SimpleAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92BFC4BA2C11BCC50051F2EA /* SimpleAnimation.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+		63A23DBBF6B9A1877F1E82A7 /* Pods_Spine_iOS_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Spine_iOS_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		7C440864BA6BF0A3BA8297E4 /* Pods-Spine iOS Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Spine iOS Example.debug.xcconfig"; path = "Target Support Files/Pods-Spine iOS Example/Pods-Spine iOS Example.debug.xcconfig"; sourceTree = "<group>"; };
+		9206EF0B2C119BC500320243 /* SpineCppLite.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SpineCppLite.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		9281EF112C07885C00BE19F5 /* Spine iOS Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Spine iOS Example.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+		9281EF142C07885C00BE19F5 /* Spine_iOS_ExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Spine_iOS_ExampleApp.swift; sourceTree = "<group>"; };
+		9281EF182C07885D00BE19F5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		9281EF1B2C07885D00BE19F5 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
+		92BFC4B22C11BCA10051F2EA /* spineboy-pro.skel */ = {isa = PBXFileReference; lastKnownFileType = file; path = "spineboy-pro.skel"; sourceTree = "<group>"; };
+		92BFC4B32C11BCA10051F2EA /* spineboy-pro.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "spineboy-pro.json"; sourceTree = "<group>"; };
+		92BFC4B42C11BCA10051F2EA /* spineboy.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = spineboy.png; sourceTree = "<group>"; };
+		92BFC4B52C11BCA10051F2EA /* spineboy.atlas */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = spineboy.atlas; sourceTree = "<group>"; };
+		92BFC4BA2C11BCC50051F2EA /* SimpleAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SimpleAnimation.swift; path = "Spine iOS Example/SimpleAnimation.swift"; sourceTree = SOURCE_ROOT; };
+		F571992938153AF59959A005 /* Pods-Spine iOS Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Spine iOS Example.release.xcconfig"; path = "Target Support Files/Pods-Spine iOS Example/Pods-Spine iOS Example.release.xcconfig"; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		9281EF0E2C07885C00BE19F5 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				029E40AACC629A2B19C75855 /* Pods_Spine_iOS_Example.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		646ABFDC442BA39836A04383 /* Pods */ = {
+			isa = PBXGroup;
+			children = (
+				7C440864BA6BF0A3BA8297E4 /* Pods-Spine iOS Example.debug.xcconfig */,
+				F571992938153AF59959A005 /* Pods-Spine iOS Example.release.xcconfig */,
+			);
+			path = Pods;
+			sourceTree = "<group>";
+		};
+		9281EF082C07885C00BE19F5 = {
+			isa = PBXGroup;
+			children = (
+				9281EF132C07885C00BE19F5 /* Spine iOS Example */,
+				9281EF122C07885C00BE19F5 /* Products */,
+				646ABFDC442BA39836A04383 /* Pods */,
+				F18879239F06E7487AA7C894 /* Frameworks */,
+			);
+			sourceTree = "<group>";
+		};
+		9281EF122C07885C00BE19F5 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				9281EF112C07885C00BE19F5 /* Spine iOS Example.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		9281EF132C07885C00BE19F5 /* Spine iOS Example */ = {
+			isa = PBXGroup;
+			children = (
+				92BFC4B12C11BC840051F2EA /* spineboy */,
+				9281EF142C07885C00BE19F5 /* Spine_iOS_ExampleApp.swift */,
+				92BFC4BA2C11BCC50051F2EA /* SimpleAnimation.swift */,
+				9281EF182C07885D00BE19F5 /* Assets.xcassets */,
+				9281EF1A2C07885D00BE19F5 /* Preview Content */,
+			);
+			path = "Spine iOS Example";
+			sourceTree = "<group>";
+		};
+		9281EF1A2C07885D00BE19F5 /* Preview Content */ = {
+			isa = PBXGroup;
+			children = (
+				9281EF1B2C07885D00BE19F5 /* Preview Assets.xcassets */,
+			);
+			path = "Preview Content";
+			sourceTree = "<group>";
+		};
+		92BFC4B12C11BC840051F2EA /* spineboy */ = {
+			isa = PBXGroup;
+			children = (
+				92BFC4B32C11BCA10051F2EA /* spineboy-pro.json */,
+				92BFC4B22C11BCA10051F2EA /* spineboy-pro.skel */,
+				92BFC4B52C11BCA10051F2EA /* spineboy.atlas */,
+				92BFC4B42C11BCA10051F2EA /* spineboy.png */,
+			);
+			path = spineboy;
+			sourceTree = "<group>";
+		};
+		F18879239F06E7487AA7C894 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				9206EF0B2C119BC500320243 /* SpineCppLite.framework */,
+				63A23DBBF6B9A1877F1E82A7 /* Pods_Spine_iOS_Example.framework */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		9281EF102C07885C00BE19F5 /* Spine iOS Example */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 9281EF1F2C07885D00BE19F5 /* Build configuration list for PBXNativeTarget "Spine iOS Example" */;
+			buildPhases = (
+				4CE732AF2F7C247F187833EC /* [CP] Check Pods Manifest.lock */,
+				9281EF0D2C07885C00BE19F5 /* Sources */,
+				9281EF0E2C07885C00BE19F5 /* Frameworks */,
+				9281EF0F2C07885C00BE19F5 /* Resources */,
+				A39FE6D793C3DED23C563AA5 /* [CP] Embed Pods Frameworks */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = "Spine iOS Example";
+			productName = "Spine iOS Example";
+			productReference = 9281EF112C07885C00BE19F5 /* Spine iOS Example.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		9281EF092C07885C00BE19F5 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				BuildIndependentTargetsInParallel = 1;
+				LastSwiftUpdateCheck = 1520;
+				LastUpgradeCheck = 1520;
+				TargetAttributes = {
+					9281EF102C07885C00BE19F5 = {
+						CreatedOnToolsVersion = 15.2;
+					};
+				};
+			};
+			buildConfigurationList = 9281EF0C2C07885C00BE19F5 /* Build configuration list for PBXProject "Spine iOS Example" */;
+			compatibilityVersion = "Xcode 14.0";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 9281EF082C07885C00BE19F5;
+			productRefGroup = 9281EF122C07885C00BE19F5 /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				9281EF102C07885C00BE19F5 /* Spine iOS Example */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		9281EF0F2C07885C00BE19F5 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				92BFC4B72C11BCA10051F2EA /* spineboy-pro.json in Resources */,
+				9281EF1C2C07885D00BE19F5 /* Preview Assets.xcassets in Resources */,
+				92BFC4B62C11BCA10051F2EA /* spineboy-pro.skel in Resources */,
+				9281EF192C07885D00BE19F5 /* Assets.xcassets in Resources */,
+				92BFC4B92C11BCA10051F2EA /* spineboy.atlas in Resources */,
+				92BFC4B82C11BCA10051F2EA /* spineboy.png in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+		4CE732AF2F7C247F187833EC /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputFileListPaths = (
+			);
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-Spine iOS Example-checkManifestLockResult.txt",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+			showEnvVarsInLog = 0;
+		};
+		A39FE6D793C3DED23C563AA5 /* [CP] Embed Pods Frameworks */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Spine iOS Example/Pods-Spine iOS Example-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+			);
+			name = "[CP] Embed Pods Frameworks";
+			outputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Spine iOS Example/Pods-Spine iOS Example-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Spine iOS Example/Pods-Spine iOS Example-frameworks.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		9281EF0D2C07885C00BE19F5 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				92BFC4BB2C11BCC50051F2EA /* SimpleAnimation.swift in Sources */,
+				9281EF152C07885C00BE19F5 /* Spine_iOS_ExampleApp.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+		9281EF1D2C07885D00BE19F5 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				ENABLE_USER_SCRIPT_SANDBOXING = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu17;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.2;
+				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				SDKROOT = iphoneos;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+			};
+			name = Debug;
+		};
+		9281EF1E2C07885D00BE19F5 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_USER_SCRIPT_SANDBOXING = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu17;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.2;
+				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				MTL_FAST_MATH = YES;
+				SDKROOT = iphoneos;
+				SWIFT_COMPILATION_MODE = wholemodule;
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
+		9281EF202C07885D00BE19F5 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = 7C440864BA6BF0A3BA8297E4 /* Pods-Spine iOS Example.debug.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_STYLE = Automatic;
+				COMPRESS_PNG_FILES = NO;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_ASSET_PATHS = "\"Spine iOS Example/Preview Content\"";
+				DEVELOPMENT_TEAM = 9ZFD4KCY8F;
+				ENABLE_PREVIEWS = YES;
+				ENABLE_USER_SCRIPT_SANDBOXING = NO;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+				INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = "com.esotericsoftware.spine-ios-example.Spine-iOS-Example";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				STRIP_PNG_TEXT = NO;
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_OBJC_INTEROP_MODE = objcxx;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		9281EF212C07885D00BE19F5 /* Release */ = {
+			isa = XCBuildConfiguration;
+			baseConfigurationReference = F571992938153AF59959A005 /* Pods-Spine iOS Example.release.xcconfig */;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CODE_SIGN_STYLE = Automatic;
+				COMPRESS_PNG_FILES = NO;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_ASSET_PATHS = "\"Spine iOS Example/Preview Content\"";
+				DEVELOPMENT_TEAM = 9ZFD4KCY8F;
+				ENABLE_PREVIEWS = YES;
+				ENABLE_USER_SCRIPT_SANDBOXING = NO;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+				INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+				INFOPLIST_KEY_UILaunchScreen_Generation = YES;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/Frameworks",
+				);
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = "com.esotericsoftware.spine-ios-example.Spine-iOS-Example";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				STRIP_PNG_TEXT = NO;
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_OBJC_INTEROP_MODE = objcxx;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		9281EF0C2C07885C00BE19F5 /* Build configuration list for PBXProject "Spine iOS Example" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				9281EF1D2C07885D00BE19F5 /* Debug */,
+				9281EF1E2C07885D00BE19F5 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		9281EF1F2C07885D00BE19F5 /* Build configuration list for PBXNativeTarget "Spine iOS Example" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				9281EF202C07885D00BE19F5 /* Debug */,
+				9281EF212C07885D00BE19F5 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+	};
+	rootObject = 9281EF092C07885C00BE19F5 /* Project object */;
+}

+ 11 - 0
spine-ios/Example - Cocoapods/Spine iOS Example/Assets.xcassets/AccentColor.colorset/Contents.json

@@ -0,0 +1,11 @@
+{
+  "colors" : [
+    {
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 13 - 0
spine-ios/Example - Cocoapods/Spine iOS Example/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,13 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "platform" : "ios",
+      "size" : "1024x1024"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 6 - 0
spine-ios/Example - Cocoapods/Spine iOS Example/Assets.xcassets/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 6 - 0
spine-ios/Example - Cocoapods/Spine iOS Example/Preview Content/Preview Assets.xcassets/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 35 - 0
spine-ios/Example - Cocoapods/Spine iOS Example/SimpleAnimation.swift

@@ -0,0 +1,35 @@
+import SwiftUI
+import Spine
+
+struct SimpleAnimation: View {
+    
+    @StateObject
+    var controller = SpineController(
+        onInitialized: { controller in
+            controller.animationState.setAnimationByName(
+                trackIndex: 0,
+                animationName: "walk",
+                loop: true
+            )
+        }
+    )
+    
+    var body: some View {
+        SpineView(
+            from: .bundle(atlasFileName: "spineboy.atlas", skeletonFileName: "spineboy-pro.skel"),
+//            from: .http(
+//                atlasURL: URL(string: "https://github.com/denrase/spine-runtimes/raw/spine-ios/spine-ios/Example/Spine%20iOS%20Example/Assets/spineboy/spineboy.atlas")!,
+//                skeletonURL:  URL(string: "https://github.com/denrase/spine-runtimes/raw/spine-ios/spine-ios/Example/Spine%20iOS%20Example/Assets/spineboy/spineboy-pro.skel")!
+//            ),
+            controller: controller,
+            mode: .fit,
+            alignment: .center
+        )
+        .navigationTitle("Simple Animation")
+        .navigationBarTitleDisplayMode(.inline)
+    }
+}
+
+#Preview {
+    SimpleAnimation()
+}

+ 18 - 0
spine-ios/Example - Cocoapods/Spine iOS Example/Spine_iOS_ExampleApp.swift

@@ -0,0 +1,18 @@
+//
+//  Spine_iOS_ExampleApp.swift
+//  Spine iOS Example
+//
+//  Created by Denis Andrašec on 29.05.24.
+//
+
+import SwiftUI
+import SpineCppLite
+
+@main
+struct Spine_iOS_ExampleApp: App {
+    var body: some Scene {
+        WindowGroup {
+            SimpleAnimation()
+        }
+    }
+}

文件差异内容过多而无法显示
+ 557 - 0
spine-ios/Example - Cocoapods/Spine iOS Example/spineboy/spineboy-pro.json


二进制
spine-ios/Example - Cocoapods/Spine iOS Example/spineboy/spineboy-pro.skel


+ 94 - 0
spine-ios/Example - Cocoapods/Spine iOS Example/spineboy/spineboy.atlas

@@ -0,0 +1,94 @@
+spineboy.png
+	size: 1024, 256
+	filter: Linear, Linear
+	scale: 0.5
+crosshair
+	bounds: 352, 7, 45, 45
+eye-indifferent
+	bounds: 862, 105, 47, 45
+eye-surprised
+	bounds: 505, 79, 47, 45
+front-bracer
+	bounds: 826, 66, 29, 40
+front-fist-closed
+	bounds: 786, 65, 38, 41
+front-fist-open
+	bounds: 710, 51, 43, 44
+	rotate: 90
+front-foot
+	bounds: 210, 6, 63, 35
+front-shin
+	bounds: 665, 128, 41, 92
+	rotate: 90
+front-thigh
+	bounds: 2, 2, 23, 56
+	rotate: 90
+front-upper-arm
+	bounds: 250, 205, 23, 49
+goggles
+	bounds: 665, 171, 131, 83
+gun
+	bounds: 798, 152, 105, 102
+head
+	bounds: 2, 27, 136, 149
+hoverboard-board
+	bounds: 2, 178, 246, 76
+hoverboard-thruster
+	bounds: 722, 96, 30, 32
+	rotate: 90
+hoverglow-small
+	bounds: 275, 81, 137, 38
+mouth-grind
+	bounds: 614, 97, 47, 30
+mouth-oooo
+	bounds: 612, 65, 47, 30
+mouth-smile
+	bounds: 661, 64, 47, 30
+muzzle-glow
+	bounds: 382, 54, 25, 25
+muzzle-ring
+	bounds: 275, 54, 25, 105
+	rotate: 90
+muzzle01
+	bounds: 911, 95, 67, 40
+	rotate: 90
+muzzle02
+	bounds: 792, 108, 68, 42
+muzzle03
+	bounds: 956, 171, 83, 53
+	rotate: 90
+muzzle04
+	bounds: 275, 7, 75, 45
+muzzle05
+	bounds: 140, 3, 68, 38
+neck
+	bounds: 250, 182, 18, 21
+portal-bg
+	bounds: 140, 43, 133, 133
+portal-flare1
+	bounds: 554, 65, 56, 30
+portal-flare2
+	bounds: 759, 112, 57, 31
+	rotate: 90
+portal-flare3
+	bounds: 554, 97, 58, 30
+portal-shade
+	bounds: 275, 121, 133, 133
+portal-streaks1
+	bounds: 410, 126, 126, 128
+portal-streaks2
+	bounds: 538, 129, 125, 125
+rear-bracer
+	bounds: 857, 67, 28, 36
+rear-foot
+	bounds: 663, 96, 57, 30
+rear-shin
+	bounds: 414, 86, 38, 89
+	rotate: 90
+rear-thigh
+	bounds: 756, 63, 28, 47
+rear-upper-arm
+	bounds: 60, 5, 20, 44
+	rotate: 90
+torso
+	bounds: 905, 164, 49, 90

二进制
spine-ios/Example - Cocoapods/Spine iOS Example/spineboy/spineboy.png


+ 91 - 0
spine-ios/Example/.gitignore

@@ -0,0 +1,91 @@
+# Xcode
+#
+# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
+
+## User settings
+xcuserdata/
+
+## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
+*.xcscmblueprint
+*.xccheckout
+
+## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
+build/
+DerivedData/
+*.moved-aside
+*.pbxuser
+!default.pbxuser
+*.mode1v3
+!default.mode1v3
+*.mode2v3
+!default.mode2v3
+*.perspectivev3
+!default.perspectivev3
+
+## Obj-C/Swift specific
+*.hmap
+
+## App packaging
+*.ipa
+*.dSYM.zip
+*.dSYM
+
+## Playgrounds
+timeline.xctimeline
+playground.xcworkspace
+
+# Swift Package Manager
+#
+# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
+# Packages/
+# Package.pins
+# Package.resolved
+# *.xcodeproj
+#
+# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
+# hence it is not needed unless you have added a package configuration file to your project
+# .swiftpm
+
+.build/
+
+# CocoaPods
+#
+# We recommend against adding the Pods directory to your .gitignore. However
+# you should judge for yourself, the pros and cons are mentioned at:
+# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
+#
+# Pods/
+#
+# Add this line if you want to avoid checking in source code from the Xcode workspace
+# *.xcworkspace
+
+# Carthage
+#
+# Add this line if you want to avoid checking in source code from Carthage dependencies.
+# Carthage/Checkouts
+
+Carthage/Build/
+
+# Accio dependency management
+Dependencies/
+.accio/
+
+# fastlane
+#
+# It is recommended to not store the screenshots in the git repo.
+# Instead, use fastlane to re-generate the screenshots whenever they are needed.
+# For more information about the recommended setup visit:
+# https://docs.fastlane.tools/best-practices/source-control/#source-control
+
+fastlane/report.xml
+fastlane/Preview.html
+fastlane/screenshots/**/*.png
+fastlane/test_output
+
+# Code Injection
+#
+# After new code Injection tools there's a generated folder /iOSInjectionProject
+# https://github.com/johnno1962/injectionforxcode
+
+iOSInjectionProject/
+.DS_store

+ 557 - 0
spine-ios/Example/Spine iOS Example.xcodeproj/project.pbxproj

@@ -0,0 +1,557 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 60;
+	objects = {
+
+/* Begin PBXBuildFile section */
+		9205FCD42C0760B1006EE07E /* SimpleAnimationViewControllerRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9205FCD32C0760B1006EE07E /* SimpleAnimationViewControllerRepresentable.swift */; };
+		920BD1162BEBC52D0050A5A9 /* spineboy-pro.skel in Resources */ = {isa = PBXBuildFile; fileRef = 920BD1122BEBC52D0050A5A9 /* spineboy-pro.skel */; };
+		920BD1182BEBC52D0050A5A9 /* spineboy-pro.json in Resources */ = {isa = PBXBuildFile; fileRef = 920BD1142BEBC52D0050A5A9 /* spineboy-pro.json */; };
+		920BD1192BEBC52D0050A5A9 /* spineboy.atlas in Resources */ = {isa = PBXBuildFile; fileRef = 920BD1152BEBC52D0050A5A9 /* spineboy.atlas */; };
+		920BD11B2BEBDBA60050A5A9 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920BD11A2BEBDBA60050A5A9 /* MainView.swift */; };
+		920BD11D2BEBDC380050A5A9 /* PlayPauseAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920BD11C2BEBDC380050A5A9 /* PlayPauseAnimation.swift */; };
+		920BD1262BEBDCD80050A5A9 /* dragon_4.png in Resources */ = {isa = PBXBuildFile; fileRef = 920BD11F2BEBDCD80050A5A9 /* dragon_4.png */; };
+		920BD1272BEBDCD80050A5A9 /* dragon_5.png in Resources */ = {isa = PBXBuildFile; fileRef = 920BD1202BEBDCD80050A5A9 /* dragon_5.png */; };
+		920BD1282BEBDCD80050A5A9 /* dragon_2.png in Resources */ = {isa = PBXBuildFile; fileRef = 920BD1212BEBDCD80050A5A9 /* dragon_2.png */; };
+		920BD1292BEBDCD80050A5A9 /* dragon-ess.skel in Resources */ = {isa = PBXBuildFile; fileRef = 920BD1222BEBDCD80050A5A9 /* dragon-ess.skel */; };
+		920BD12A2BEBDCD80050A5A9 /* dragon_3.png in Resources */ = {isa = PBXBuildFile; fileRef = 920BD1232BEBDCD80050A5A9 /* dragon_3.png */; };
+		920BD12B2BEBDCD80050A5A9 /* dragon.png in Resources */ = {isa = PBXBuildFile; fileRef = 920BD1242BEBDCD80050A5A9 /* dragon.png */; };
+		920BD12C2BEBDCD80050A5A9 /* dragon.atlas in Resources */ = {isa = PBXBuildFile; fileRef = 920BD1252BEBDCD80050A5A9 /* dragon.atlas */; };
+		921C1EAF2BF609F60001A0BA /* DressUp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 921C1EAE2BF609F60001A0BA /* DressUp.swift */; };
+		921C1EBA2BF60AE90001A0BA /* mix-and-match-pro.skel in Resources */ = {isa = PBXBuildFile; fileRef = 921C1EB72BF60AE90001A0BA /* mix-and-match-pro.skel */; };
+		921C1EBB2BF60AE90001A0BA /* mix-and-match.atlas in Resources */ = {isa = PBXBuildFile; fileRef = 921C1EB82BF60AE90001A0BA /* mix-and-match.atlas */; };
+		921C1EBC2BF60AE90001A0BA /* mix-and-match.png in Resources */ = {isa = PBXBuildFile; fileRef = 921C1EB92BF60AE90001A0BA /* mix-and-match.png */; };
+		922839DF2BF4F7D8006DA9F6 /* AnimationStateEvents.swift in Sources */ = {isa = PBXBuildFile; fileRef = 922839DE2BF4F7D8006DA9F6 /* AnimationStateEvents.swift */; };
+		9240C26E2C0754DD003EE243 /* SimpleAnimationViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9240C26D2C0754DD003EE243 /* SimpleAnimationViewController.m */; };
+		924C0C162BCFCF21004E63F7 /* SpineExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 924C0C152BCFCF21004E63F7 /* SpineExampleApp.swift */; };
+		924C0C182BCFCF21004E63F7 /* SimpleAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 924C0C172BCFCF21004E63F7 /* SimpleAnimation.swift */; };
+		924C0C1A2BCFCF22004E63F7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 924C0C192BCFCF22004E63F7 /* Assets.xcassets */; };
+		925315522BFF994400C96F75 /* spineboy.png in Resources */ = {isa = PBXBuildFile; fileRef = 925315512BFF994400C96F75 /* spineboy.png */; };
+		92579E772C1B0E9500FDC7D5 /* DisableRendering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92579E762C1B0E9500FDC7D5 /* DisableRendering.swift */; };
+		925CB7E92C19BC5A00C8F47F /* Spine in Frameworks */ = {isa = PBXBuildFile; productRef = 925CB7E82C19BC5A00C8F47F /* Spine */; };
+		9270C16E2BFE356000BD25BC /* Physics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9270C16D2BFE356000BD25BC /* Physics.swift */; };
+		9270C1732BFE389600BD25BC /* celestial-circus.png in Resources */ = {isa = PBXBuildFile; fileRef = 9270C1702BFE389600BD25BC /* celestial-circus.png */; };
+		9270C1742BFE389600BD25BC /* celestial-circus-pro.skel in Resources */ = {isa = PBXBuildFile; fileRef = 9270C1712BFE389600BD25BC /* celestial-circus-pro.skel */; };
+		9270C1752BFE389600BD25BC /* celestial-circus.atlas in Resources */ = {isa = PBXBuildFile; fileRef = 9270C1722BFE389600BD25BC /* celestial-circus.atlas */; };
+		928A8CC22BCFE7DF00D9D35B /* Spine in Frameworks */ = {isa = PBXBuildFile; productRef = 928A8CC12BCFE7DF00D9D35B /* Spine */; };
+		92D7DDA82BFF3C8800EB9E3A /* DebugRendering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D7DDA72BFF3C8800EB9E3A /* DebugRendering.swift */; };
+		92FE93292BF4AB9600CCDF48 /* IKFollowing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92FE93282BF4AB9600CCDF48 /* IKFollowing.swift */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+		9205FCD32C0760B1006EE07E /* SimpleAnimationViewControllerRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleAnimationViewControllerRepresentable.swift; sourceTree = "<group>"; };
+		920BD1122BEBC52D0050A5A9 /* spineboy-pro.skel */ = {isa = PBXFileReference; lastKnownFileType = file; path = "spineboy-pro.skel"; sourceTree = "<group>"; };
+		920BD1142BEBC52D0050A5A9 /* spineboy-pro.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "spineboy-pro.json"; sourceTree = "<group>"; };
+		920BD1152BEBC52D0050A5A9 /* spineboy.atlas */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = spineboy.atlas; sourceTree = "<group>"; };
+		920BD11A2BEBDBA60050A5A9 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
+		920BD11C2BEBDC380050A5A9 /* PlayPauseAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayPauseAnimation.swift; sourceTree = "<group>"; };
+		920BD11F2BEBDCD80050A5A9 /* dragon_4.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = dragon_4.png; sourceTree = "<group>"; };
+		920BD1202BEBDCD80050A5A9 /* dragon_5.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = dragon_5.png; sourceTree = "<group>"; };
+		920BD1212BEBDCD80050A5A9 /* dragon_2.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = dragon_2.png; sourceTree = "<group>"; };
+		920BD1222BEBDCD80050A5A9 /* dragon-ess.skel */ = {isa = PBXFileReference; lastKnownFileType = file; path = "dragon-ess.skel"; sourceTree = "<group>"; };
+		920BD1232BEBDCD80050A5A9 /* dragon_3.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = dragon_3.png; sourceTree = "<group>"; };
+		920BD1242BEBDCD80050A5A9 /* dragon.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = dragon.png; sourceTree = "<group>"; };
+		920BD1252BEBDCD80050A5A9 /* dragon.atlas */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = dragon.atlas; sourceTree = "<group>"; };
+		921C1EAE2BF609F60001A0BA /* DressUp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DressUp.swift; sourceTree = "<group>"; };
+		921C1EB72BF60AE90001A0BA /* mix-and-match-pro.skel */ = {isa = PBXFileReference; lastKnownFileType = file; path = "mix-and-match-pro.skel"; sourceTree = "<group>"; };
+		921C1EB82BF60AE90001A0BA /* mix-and-match.atlas */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "mix-and-match.atlas"; sourceTree = "<group>"; };
+		921C1EB92BF60AE90001A0BA /* mix-and-match.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "mix-and-match.png"; sourceTree = "<group>"; };
+		922839DE2BF4F7D8006DA9F6 /* AnimationStateEvents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationStateEvents.swift; sourceTree = "<group>"; };
+		9240C26B2C0754DC003EE243 /* Spine iOS Example-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Spine iOS Example-Bridging-Header.h"; sourceTree = "<group>"; };
+		9240C26C2C0754DD003EE243 /* SimpleAnimationViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SimpleAnimationViewController.h; sourceTree = "<group>"; };
+		9240C26D2C0754DD003EE243 /* SimpleAnimationViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SimpleAnimationViewController.m; sourceTree = "<group>"; };
+		924C0C122BCFCF21004E63F7 /* Spine iOS Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Spine iOS Example.app"; sourceTree = BUILT_PRODUCTS_DIR; };
+		924C0C152BCFCF21004E63F7 /* SpineExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpineExampleApp.swift; sourceTree = "<group>"; };
+		924C0C172BCFCF21004E63F7 /* SimpleAnimation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleAnimation.swift; sourceTree = "<group>"; };
+		924C0C192BCFCF22004E63F7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
+		924C0C1B2BCFCF22004E63F7 /* SpineiOSExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = SpineiOSExample.entitlements; sourceTree = "<group>"; };
+		925315512BFF994400C96F75 /* spineboy.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = spineboy.png; sourceTree = "<group>"; };
+		92579E762C1B0E9500FDC7D5 /* DisableRendering.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisableRendering.swift; sourceTree = "<group>"; };
+		9270C16D2BFE356000BD25BC /* Physics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Physics.swift; sourceTree = "<group>"; };
+		9270C1702BFE389600BD25BC /* celestial-circus.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "celestial-circus.png"; sourceTree = "<group>"; };
+		9270C1712BFE389600BD25BC /* celestial-circus-pro.skel */ = {isa = PBXFileReference; lastKnownFileType = file; path = "celestial-circus-pro.skel"; sourceTree = "<group>"; };
+		9270C1722BFE389600BD25BC /* celestial-circus.atlas */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "celestial-circus.atlas"; sourceTree = "<group>"; };
+		92D7DDA72BFF3C8800EB9E3A /* DebugRendering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugRendering.swift; sourceTree = "<group>"; };
+		92FE93282BF4AB9600CCDF48 /* IKFollowing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IKFollowing.swift; sourceTree = "<group>"; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+		924C0C0F2BCFCF21004E63F7 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				925CB7E92C19BC5A00C8F47F /* Spine in Frameworks */,
+				928A8CC22BCFE7DF00D9D35B /* Spine in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		920BD1112BEBC52D0050A5A9 /* spineboy */ = {
+			isa = PBXGroup;
+			children = (
+				925315512BFF994400C96F75 /* spineboy.png */,
+				920BD1122BEBC52D0050A5A9 /* spineboy-pro.skel */,
+				920BD1142BEBC52D0050A5A9 /* spineboy-pro.json */,
+				920BD1152BEBC52D0050A5A9 /* spineboy.atlas */,
+			);
+			path = spineboy;
+			sourceTree = "<group>";
+		};
+		920BD11E2BEBDCD80050A5A9 /* dragon */ = {
+			isa = PBXGroup;
+			children = (
+				920BD11F2BEBDCD80050A5A9 /* dragon_4.png */,
+				920BD1202BEBDCD80050A5A9 /* dragon_5.png */,
+				920BD1212BEBDCD80050A5A9 /* dragon_2.png */,
+				920BD1222BEBDCD80050A5A9 /* dragon-ess.skel */,
+				920BD1232BEBDCD80050A5A9 /* dragon_3.png */,
+				920BD1242BEBDCD80050A5A9 /* dragon.png */,
+				920BD1252BEBDCD80050A5A9 /* dragon.atlas */,
+			);
+			path = dragon;
+			sourceTree = "<group>";
+		};
+		921C1EB02BF60AA40001A0BA /* mixandmatch */ = {
+			isa = PBXGroup;
+			children = (
+				921C1EB72BF60AE90001A0BA /* mix-and-match-pro.skel */,
+				921C1EB82BF60AE90001A0BA /* mix-and-match.atlas */,
+				921C1EB92BF60AE90001A0BA /* mix-and-match.png */,
+			);
+			path = mixandmatch;
+			sourceTree = "<group>";
+		};
+		924C0C092BCFCF21004E63F7 = {
+			isa = PBXGroup;
+			children = (
+				924C0C142BCFCF21004E63F7 /* Spine iOS Example */,
+				924C0C132BCFCF21004E63F7 /* Products */,
+				92CE2BE32BCFE57600E9B376 /* Frameworks */,
+			);
+			sourceTree = "<group>";
+		};
+		924C0C132BCFCF21004E63F7 /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				924C0C122BCFCF21004E63F7 /* Spine iOS Example.app */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
+		924C0C142BCFCF21004E63F7 /* Spine iOS Example */ = {
+			isa = PBXGroup;
+			children = (
+				92921E502BCFEEA1002E2FC4 /* Assets */,
+				924C0C152BCFCF21004E63F7 /* SpineExampleApp.swift */,
+				920BD11A2BEBDBA60050A5A9 /* MainView.swift */,
+				924C0C172BCFCF21004E63F7 /* SimpleAnimation.swift */,
+				9205FCD32C0760B1006EE07E /* SimpleAnimationViewControllerRepresentable.swift */,
+				9240C26C2C0754DD003EE243 /* SimpleAnimationViewController.h */,
+				9240C26D2C0754DD003EE243 /* SimpleAnimationViewController.m */,
+				920BD11C2BEBDC380050A5A9 /* PlayPauseAnimation.swift */,
+				922839DE2BF4F7D8006DA9F6 /* AnimationStateEvents.swift */,
+				92D7DDA72BFF3C8800EB9E3A /* DebugRendering.swift */,
+				921C1EAE2BF609F60001A0BA /* DressUp.swift */,
+				92FE93282BF4AB9600CCDF48 /* IKFollowing.swift */,
+				9270C16D2BFE356000BD25BC /* Physics.swift */,
+				92579E762C1B0E9500FDC7D5 /* DisableRendering.swift */,
+				924C0C192BCFCF22004E63F7 /* Assets.xcassets */,
+				924C0C1B2BCFCF22004E63F7 /* SpineiOSExample.entitlements */,
+				9240C26B2C0754DC003EE243 /* Spine iOS Example-Bridging-Header.h */,
+			);
+			path = "Spine iOS Example";
+			sourceTree = "<group>";
+		};
+		9270C16F2BFE387600BD25BC /* celestial */ = {
+			isa = PBXGroup;
+			children = (
+				9270C1712BFE389600BD25BC /* celestial-circus-pro.skel */,
+				9270C1722BFE389600BD25BC /* celestial-circus.atlas */,
+				9270C1702BFE389600BD25BC /* celestial-circus.png */,
+			);
+			path = celestial;
+			sourceTree = "<group>";
+		};
+		92921E502BCFEEA1002E2FC4 /* Assets */ = {
+			isa = PBXGroup;
+			children = (
+				9270C16F2BFE387600BD25BC /* celestial */,
+				921C1EB02BF60AA40001A0BA /* mixandmatch */,
+				920BD11E2BEBDCD80050A5A9 /* dragon */,
+				920BD1112BEBC52D0050A5A9 /* spineboy */,
+			);
+			path = Assets;
+			sourceTree = "<group>";
+		};
+		92CE2BE32BCFE57600E9B376 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+		924C0C112BCFCF21004E63F7 /* Spine iOS Example */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 924C0C212BCFCF22004E63F7 /* Build configuration list for PBXNativeTarget "Spine iOS Example" */;
+			buildPhases = (
+				924C0C0E2BCFCF21004E63F7 /* Sources */,
+				924C0C0F2BCFCF21004E63F7 /* Frameworks */,
+				924C0C102BCFCF21004E63F7 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = "Spine iOS Example";
+			packageProductDependencies = (
+				928A8CC12BCFE7DF00D9D35B /* Spine */,
+				925CB7E82C19BC5A00C8F47F /* Spine */,
+			);
+			productName = "Spine iOS Example";
+			productReference = 924C0C122BCFCF21004E63F7 /* Spine iOS Example.app */;
+			productType = "com.apple.product-type.application";
+		};
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+		924C0C0A2BCFCF21004E63F7 /* Project object */ = {
+			isa = PBXProject;
+			attributes = {
+				BuildIndependentTargetsInParallel = 1;
+				LastSwiftUpdateCheck = 1520;
+				LastUpgradeCheck = 1520;
+				TargetAttributes = {
+					924C0C112BCFCF21004E63F7 = {
+						CreatedOnToolsVersion = 15.2;
+						LastSwiftMigration = 1520;
+					};
+				};
+			};
+			buildConfigurationList = 924C0C0D2BCFCF21004E63F7 /* Build configuration list for PBXProject "Spine iOS Example" */;
+			compatibilityVersion = "Xcode 14.0";
+			developmentRegion = en;
+			hasScannedForEncodings = 0;
+			knownRegions = (
+				en,
+				Base,
+			);
+			mainGroup = 924C0C092BCFCF21004E63F7;
+			packageReferences = (
+				925CB7E72C19BC5A00C8F47F /* XCLocalSwiftPackageReference "../.." */,
+			);
+			productRefGroup = 924C0C132BCFCF21004E63F7 /* Products */;
+			projectDirPath = "";
+			projectRoot = "";
+			targets = (
+				924C0C112BCFCF21004E63F7 /* Spine iOS Example */,
+			);
+		};
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+		924C0C102BCFCF21004E63F7 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				920BD12A2BEBDCD80050A5A9 /* dragon_3.png in Resources */,
+				924C0C1A2BCFCF22004E63F7 /* Assets.xcassets in Resources */,
+				920BD1282BEBDCD80050A5A9 /* dragon_2.png in Resources */,
+				920BD1182BEBC52D0050A5A9 /* spineboy-pro.json in Resources */,
+				9270C1752BFE389600BD25BC /* celestial-circus.atlas in Resources */,
+				9270C1742BFE389600BD25BC /* celestial-circus-pro.skel in Resources */,
+				921C1EBB2BF60AE90001A0BA /* mix-and-match.atlas in Resources */,
+				9270C1732BFE389600BD25BC /* celestial-circus.png in Resources */,
+				925315522BFF994400C96F75 /* spineboy.png in Resources */,
+				920BD1192BEBC52D0050A5A9 /* spineboy.atlas in Resources */,
+				920BD1292BEBDCD80050A5A9 /* dragon-ess.skel in Resources */,
+				920BD1162BEBC52D0050A5A9 /* spineboy-pro.skel in Resources */,
+				920BD12B2BEBDCD80050A5A9 /* dragon.png in Resources */,
+				920BD1272BEBDCD80050A5A9 /* dragon_5.png in Resources */,
+				921C1EBC2BF60AE90001A0BA /* mix-and-match.png in Resources */,
+				920BD1262BEBDCD80050A5A9 /* dragon_4.png in Resources */,
+				920BD12C2BEBDCD80050A5A9 /* dragon.atlas in Resources */,
+				921C1EBA2BF60AE90001A0BA /* mix-and-match-pro.skel in Resources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+		924C0C0E2BCFCF21004E63F7 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				9205FCD42C0760B1006EE07E /* SimpleAnimationViewControllerRepresentable.swift in Sources */,
+				9270C16E2BFE356000BD25BC /* Physics.swift in Sources */,
+				920BD11B2BEBDBA60050A5A9 /* MainView.swift in Sources */,
+				92D7DDA82BFF3C8800EB9E3A /* DebugRendering.swift in Sources */,
+				92FE93292BF4AB9600CCDF48 /* IKFollowing.swift in Sources */,
+				920BD11D2BEBDC380050A5A9 /* PlayPauseAnimation.swift in Sources */,
+				9240C26E2C0754DD003EE243 /* SimpleAnimationViewController.m in Sources */,
+				921C1EAF2BF609F60001A0BA /* DressUp.swift in Sources */,
+				922839DF2BF4F7D8006DA9F6 /* AnimationStateEvents.swift in Sources */,
+				924C0C182BCFCF21004E63F7 /* SimpleAnimation.swift in Sources */,
+				924C0C162BCFCF21004E63F7 /* SpineExampleApp.swift in Sources */,
+				92579E772C1B0E9500FDC7D5 /* DisableRendering.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+		924C0C1F2BCFCF22004E63F7 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COMPRESS_PNG_FILES = NO;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = dwarf;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_TESTABILITY = YES;
+				ENABLE_USER_SCRIPT_SANDBOXING = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu17;
+				GCC_DYNAMIC_NO_PIC = NO;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_OPTIMIZATION_LEVEL = 0;
+				GCC_PREPROCESSOR_DEFINITIONS = (
+					"DEBUG=1",
+					"$(inherited)",
+				);
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+				MTL_FAST_MATH = YES;
+				ONLY_ACTIVE_ARCH = YES;
+				STRIP_PNG_TEXT = NO;
+				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+			};
+			name = Debug;
+		};
+		924C0C202BCFCF22004E63F7 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ALWAYS_SEARCH_USER_PATHS = NO;
+				ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+				CLANG_ANALYZER_NONNULL = YES;
+				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+				CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+				CLANG_ENABLE_MODULES = YES;
+				CLANG_ENABLE_OBJC_ARC = YES;
+				CLANG_ENABLE_OBJC_WEAK = YES;
+				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+				CLANG_WARN_BOOL_CONVERSION = YES;
+				CLANG_WARN_COMMA = YES;
+				CLANG_WARN_CONSTANT_CONVERSION = YES;
+				CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+				CLANG_WARN_EMPTY_BODY = YES;
+				CLANG_WARN_ENUM_CONVERSION = YES;
+				CLANG_WARN_INFINITE_RECURSION = YES;
+				CLANG_WARN_INT_CONVERSION = YES;
+				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+				CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+				CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+				CLANG_WARN_STRICT_PROTOTYPES = YES;
+				CLANG_WARN_SUSPICIOUS_MOVE = YES;
+				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+				CLANG_WARN_UNREACHABLE_CODE = YES;
+				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+				COMPRESS_PNG_FILES = NO;
+				COPY_PHASE_STRIP = NO;
+				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+				ENABLE_NS_ASSERTIONS = NO;
+				ENABLE_STRICT_OBJC_MSGSEND = YES;
+				ENABLE_USER_SCRIPT_SANDBOXING = YES;
+				GCC_C_LANGUAGE_STANDARD = gnu17;
+				GCC_NO_COMMON_BLOCKS = YES;
+				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+				GCC_WARN_UNDECLARED_SELECTOR = YES;
+				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+				GCC_WARN_UNUSED_FUNCTION = YES;
+				GCC_WARN_UNUSED_VARIABLE = YES;
+				LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+				MTL_ENABLE_DEBUG_INFO = NO;
+				MTL_FAST_MATH = YES;
+				STRIP_PNG_TEXT = NO;
+				SWIFT_COMPILATION_MODE = wholemodule;
+			};
+			name = Release;
+		};
+		924C0C222BCFCF22004E63F7 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CLANG_ENABLE_MODULES = YES;
+				CODE_SIGN_ENTITLEMENTS = "Spine iOS Example/SpineiOSExample.entitlements";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_ASSET_PATHS = "";
+				DEVELOPMENT_TEAM = 9ZFD4KCY8F;
+				ENABLE_HARDENED_RUNTIME = YES;
+				ENABLE_PREVIEWS = YES;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_FILE = "";
+				"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
+				"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
+				"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
+				"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
+				"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
+				"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
+				"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
+				"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
+				LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
+				"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
+				MACOSX_DEPLOYMENT_TARGET = 14.2;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = "com.esotericsoftware.spine-ios-example";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SDKROOT = auto;
+				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+				SUPPORTS_MACCATALYST = NO;
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_OBJC_BRIDGING_HEADER = "Spine iOS Example/Spine iOS Example-Bridging-Header.h";
+				SWIFT_OBJC_INTEROP_MODE = objcxx;
+				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Debug;
+		};
+		924C0C232BCFCF22004E63F7 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+				ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+				CLANG_ENABLE_MODULES = YES;
+				CODE_SIGN_ENTITLEMENTS = "Spine iOS Example/SpineiOSExample.entitlements";
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_ASSET_PATHS = "";
+				DEVELOPMENT_TEAM = 9ZFD4KCY8F;
+				ENABLE_HARDENED_RUNTIME = YES;
+				ENABLE_PREVIEWS = YES;
+				GENERATE_INFOPLIST_FILE = YES;
+				INFOPLIST_FILE = "";
+				"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphoneos*]" = YES;
+				"INFOPLIST_KEY_UIApplicationSceneManifest_Generation[sdk=iphonesimulator*]" = YES;
+				"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphoneos*]" = YES;
+				"INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents[sdk=iphonesimulator*]" = YES;
+				"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphoneos*]" = YES;
+				"INFOPLIST_KEY_UILaunchScreen_Generation[sdk=iphonesimulator*]" = YES;
+				"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphoneos*]" = UIStatusBarStyleDefault;
+				"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+				IPHONEOS_DEPLOYMENT_TARGET = 14.0;
+				LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
+				"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
+				MACOSX_DEPLOYMENT_TARGET = 14.2;
+				MARKETING_VERSION = 1.0;
+				PRODUCT_BUNDLE_IDENTIFIER = "com.esotericsoftware.spine-ios-example";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+				SDKROOT = auto;
+				SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+				SUPPORTS_MACCATALYST = NO;
+				SWIFT_EMIT_LOC_STRINGS = YES;
+				SWIFT_OBJC_BRIDGING_HEADER = "Spine iOS Example/Spine iOS Example-Bridging-Header.h";
+				SWIFT_OBJC_INTEROP_MODE = objcxx;
+				SWIFT_VERSION = 5.0;
+				TARGETED_DEVICE_FAMILY = "1,2";
+			};
+			name = Release;
+		};
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+		924C0C0D2BCFCF21004E63F7 /* Build configuration list for PBXProject "Spine iOS Example" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				924C0C1F2BCFCF22004E63F7 /* Debug */,
+				924C0C202BCFCF22004E63F7 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+		924C0C212BCFCF22004E63F7 /* Build configuration list for PBXNativeTarget "Spine iOS Example" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				924C0C222BCFCF22004E63F7 /* Debug */,
+				924C0C232BCFCF22004E63F7 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
+/* End XCConfigurationList section */
+
+/* Begin XCLocalSwiftPackageReference section */
+		925CB7E72C19BC5A00C8F47F /* XCLocalSwiftPackageReference "../.." */ = {
+			isa = XCLocalSwiftPackageReference;
+			relativePath = ../..;
+		};
+/* End XCLocalSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+		925CB7E82C19BC5A00C8F47F /* Spine */ = {
+			isa = XCSwiftPackageProductDependency;
+			productName = Spine;
+		};
+		928A8CC12BCFE7DF00D9D35B /* Spine */ = {
+			isa = XCSwiftPackageProductDependency;
+			productName = Spine;
+		};
+/* End XCSwiftPackageProductDependency section */
+	};
+	rootObject = 924C0C0A2BCFCF21004E63F7 /* Project object */;
+}

+ 7 - 0
spine-ios/Example/Spine iOS Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Workspace
+   version = "1.0">
+   <FileRef
+      location = "self:">
+   </FileRef>
+</Workspace>

+ 48 - 0
spine-ios/Example/Spine iOS Example/AnimationStateEvents.swift

@@ -0,0 +1,48 @@
+import SwiftUI
+import Spine
+import SpineCppLite
+
+struct AnimationStateEvents: View {
+    
+    @StateObject
+    var controller = SpineController(
+        onInitialized: { controller in
+            controller.skeleton.scaleX = 0.5
+            controller.skeleton.scaleY = 0.5
+            controller.skeleton.findSlot(slotName: "gun")?.setColor(r: 1, g: 0, b: 0, a: 1)
+            controller.animationStateData.defaultMix = 0.2
+            let walk = controller.animationState.setAnimationByName(trackIndex: 0, animationName: "walk", loop: true)
+            controller.animationStateWrapper.setTrackEntryListener(entry: walk) { type, entry, event in
+                print("Walk animation event \(type)");
+            }
+            controller.animationState.addAnimationByName(trackIndex: 0, animationName: "jump", loop: false, delay: 2)
+            let run = controller.animationState.addAnimationByName(trackIndex: 0, animationName: "run", loop: true, delay: 0)
+            controller.animationStateWrapper.setTrackEntryListener(entry: run) { type, entry, event in
+                print("Run animation event \(type)");
+            }
+            controller.animationStateWrapper.setStateListener { type, entry, event in
+                if type == SPINE_EVENT_TYPE_EVENT, let event {
+                    print("User event: { name: \(event.data.name ?? "--"), intValue: \(event.intValue), floatValue: \(event.floatValue), stringValue: \(event.stringValue ?? "--") }")
+                }
+            }
+            let current = controller.animationState.getCurrent(trackIndex: 0)?.animation.name ?? "--"
+            print("Current: \(current)")
+        }
+    )
+    
+    var body: some View {
+        VStack {
+            Text("See output in console!")
+            SpineView(
+                from: .bundle(atlasFileName: "spineboy.atlas", skeletonFileName: "spineboy-pro.skel"),
+                controller: controller
+            )
+        }
+        .navigationTitle("Animation State Listener")
+        .navigationBarTitleDisplayMode(.inline)
+    }
+}
+
+#Preview {
+    AnimationStateEvents()
+}

+ 11 - 0
spine-ios/Example/Spine iOS Example/Assets.xcassets/AccentColor.colorset/Contents.json

@@ -0,0 +1,11 @@
+{
+  "colors" : [
+    {
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 63 - 0
spine-ios/Example/Spine iOS Example/Assets.xcassets/AppIcon.appiconset/Contents.json

@@ -0,0 +1,63 @@
+{
+  "images" : [
+    {
+      "idiom" : "universal",
+      "platform" : "ios",
+      "size" : "1024x1024"
+    },
+    {
+      "idiom" : "mac",
+      "scale" : "1x",
+      "size" : "16x16"
+    },
+    {
+      "idiom" : "mac",
+      "scale" : "2x",
+      "size" : "16x16"
+    },
+    {
+      "idiom" : "mac",
+      "scale" : "1x",
+      "size" : "32x32"
+    },
+    {
+      "idiom" : "mac",
+      "scale" : "2x",
+      "size" : "32x32"
+    },
+    {
+      "idiom" : "mac",
+      "scale" : "1x",
+      "size" : "128x128"
+    },
+    {
+      "idiom" : "mac",
+      "scale" : "2x",
+      "size" : "128x128"
+    },
+    {
+      "idiom" : "mac",
+      "scale" : "1x",
+      "size" : "256x256"
+    },
+    {
+      "idiom" : "mac",
+      "scale" : "2x",
+      "size" : "256x256"
+    },
+    {
+      "idiom" : "mac",
+      "scale" : "1x",
+      "size" : "512x512"
+    },
+    {
+      "idiom" : "mac",
+      "scale" : "2x",
+      "size" : "512x512"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

+ 6 - 0
spine-ios/Example/Spine iOS Example/Assets.xcassets/Contents.json

@@ -0,0 +1,6 @@
+{
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

二进制
spine-ios/Example/Spine iOS Example/Assets/celestial/celestial-circus-pro.skel


+ 173 - 0
spine-ios/Example/Spine iOS Example/Assets/celestial/celestial-circus.atlas

@@ -0,0 +1,173 @@
+celestial-circus.png
+	size: 1024, 1024
+	filter: Linear, Linear
+	scale: 0.4
+arm-back-down
+	bounds: 324, 401, 38, 82
+	rotate: 90
+arm-back-up
+	bounds: 290, 44, 83, 116
+	rotate: 90
+arm-front-down
+	bounds: 706, 2, 36, 78
+	rotate: 90
+arm-front-up
+	bounds: 860, 138, 77, 116
+bench
+	bounds: 725, 256, 189, 48
+body-bottom
+	bounds: 879, 868, 154, 124
+	rotate: 90
+body-top
+	bounds: 725, 128, 126, 133
+	rotate: 90
+chest
+	bounds: 408, 26, 104, 93
+cloud-back
+	bounds: 752, 378, 202, 165
+cloud-front
+	bounds: 2, 2, 325, 196
+	rotate: 90
+collar
+	bounds: 786, 13, 47, 26
+ear
+	bounds: 1002, 643, 20, 28
+eye-back-shadow
+	bounds: 428, 395, 14, 10
+eye-front-shadow
+	bounds: 704, 529, 24, 14
+eye-reflex-back
+	bounds: 860, 128, 8, 7
+	rotate: 90
+eye-reflex-front
+	bounds: 726, 386, 10, 7
+eye-white-back
+	bounds: 835, 23, 13, 16
+eye-white-front
+	bounds: 1005, 1000, 22, 17
+	rotate: 90
+eyelashes-down-back
+	bounds: 232, 329, 11, 6
+	rotate: 90
+eyelashes-down-front
+	bounds: 913, 851, 15, 6
+	rotate: 90
+eyelashes-top-back
+	bounds: 408, 395, 18, 10
+eyelashes-top-front
+	bounds: 702, 179, 30, 16
+	rotate: 90
+face
+	bounds: 514, 26, 93, 102
+	rotate: 90
+feathers-back
+	bounds: 954, 625, 46, 46
+feathers-front
+	bounds: 706, 40, 72, 86
+fringe-middle-back
+	bounds: 200, 6, 33, 52
+	rotate: 90
+fringe-middle-front
+	bounds: 878, 76, 60, 50
+	rotate: 90
+fringe-side-back
+	bounds: 780, 41, 27, 94
+	rotate: 90
+fringe-side-front
+	bounds: 939, 161, 26, 93
+glove-bottom-back
+	bounds: 954, 572, 51, 41
+	rotate: 90
+glove-bottom-front
+	bounds: 916, 256, 47, 48
+hair-back-1
+	bounds: 444, 395, 132, 306
+	rotate: 90
+hair-back-2
+	bounds: 438, 211, 80, 285
+	rotate: 90
+hair-back-3
+	bounds: 719, 306, 70, 268
+	rotate: 90
+hair-back-4
+	bounds: 438, 121, 88, 262
+	rotate: 90
+hair-back-5
+	bounds: 438, 293, 88, 279
+	rotate: 90
+hair-back-6
+	bounds: 200, 41, 88, 286
+hair-hat-shadow
+	bounds: 232, 398, 90, 41
+hand-back
+	bounds: 954, 673, 60, 47
+	rotate: 90
+hand-front
+	bounds: 967, 172, 53, 60
+hat-back
+	bounds: 954, 802, 64, 45
+	rotate: 90
+hat-front
+	bounds: 780, 70, 96, 56
+head-back
+	bounds: 618, 17, 102, 86
+	rotate: 90
+jabot
+	bounds: 967, 234, 70, 55
+	rotate: 90
+leg-back
+	bounds: 232, 441, 210, 333
+leg-front
+	bounds: 444, 529, 258, 320
+logo-brooch
+	bounds: 954, 545, 16, 25
+mouth
+	bounds: 408, 121, 22, 6
+neck
+	bounds: 232, 342, 39, 56
+	rotate: 90
+nose
+	bounds: 742, 529, 6, 7
+	rotate: 90
+nose-highlight
+	bounds: 719, 300, 4, 4
+nose-shadow
+	bounds: 869, 128, 7, 8
+pupil-back
+	bounds: 730, 529, 10, 14
+pupil-front
+	bounds: 254, 21, 12, 18
+rope-back
+	bounds: 232, 383, 10, 492
+	rotate: 90
+rope-front
+	bounds: 232, 383, 10, 492
+	rotate: 90
+rope-front-bottom
+	bounds: 954, 735, 42, 65
+skirt
+	bounds: 2, 776, 440, 246
+sock-bow
+	bounds: 408, 407, 33, 32
+spine-logo-body
+	bounds: 879, 853, 13, 32
+	rotate: 90
+star-big
+	bounds: 939, 141, 18, 24
+	rotate: 90
+star-medium
+	bounds: 742, 537, 6, 8
+	rotate: 90
+star-small
+	bounds: 719, 378, 3, 4
+	rotate: 90
+underskirt
+	bounds: 2, 329, 445, 228
+	rotate: 90
+underskirt-back
+	bounds: 444, 851, 433, 171
+wing-back
+	bounds: 290, 129, 146, 252
+wing-front
+	bounds: 704, 545, 304, 248
+	rotate: 90

二进制
spine-ios/Example/Spine iOS Example/Assets/celestial/celestial-circus.png


二进制
spine-ios/Example/Spine iOS Example/Assets/dragon/dragon-ess.skel


+ 112 - 0
spine-ios/Example/Spine iOS Example/Assets/dragon/dragon.atlas

@@ -0,0 +1,112 @@
+dragon.png
+	size: 1024, 1024
+	filter: Linear, Linear
+front-toe-a
+	bounds: 797, 381, 29, 50
+front-toe-b
+	bounds: 942, 118, 56, 57
+head
+	bounds: 647, 81, 296, 260
+	rotate: 90
+left-front-leg
+	bounds: 942, 250, 84, 57
+	rotate: 90
+left-front-thigh
+	bounds: 852, 7, 84, 72
+left-wing01
+	bounds: 736, 433, 264, 589
+right-rear-toe
+	bounds: 647, 2, 109, 77
+right-wing01
+	bounds: 2, 379, 365, 643
+right-wing02
+	bounds: 369, 379, 365, 643
+right-wing03
+	bounds: 2, 12, 365, 643
+	rotate: 90
+tail03
+	bounds: 758, 6, 73, 92
+	rotate: 90
+tail04
+	bounds: 942, 177, 56, 71
+tail05
+	bounds: 736, 379, 52, 59
+	rotate: 90
+tail06
+	bounds: 942, 336, 95, 68
+	rotate: 90
+thiagobrayner
+	bounds: 909, 81, 350, 31
+	rotate: 90
+
+dragon_2.png
+	size: 1024, 1024
+	filter: Linear, Linear
+back
+	bounds: 795, 32, 190, 185
+chin
+	bounds: 647, 157, 214, 146
+	rotate: 90
+left-rear-leg
+	bounds: 795, 219, 206, 177
+	rotate: 90
+left-wing02
+	bounds: 736, 427, 264, 589
+right-wing04
+	bounds: 2, 373, 365, 643
+right-wing05
+	bounds: 369, 373, 365, 643
+right-wing06
+	bounds: 2, 6, 365, 643
+	rotate: 90
+tail01
+	bounds: 647, 2, 120, 153
+
+dragon_3.png
+	size: 1024, 1024
+	filter: Linear, Linear
+chest
+	bounds: 740, 299, 136, 122
+left-rear-thigh
+	bounds: 647, 218, 91, 149
+left-wing03
+	bounds: 736, 423, 264, 589
+right-front-leg
+	bounds: 850, 196, 101, 89
+	rotate: 90
+right-front-thigh
+	bounds: 740, 189, 108, 108
+right-rear-leg
+	bounds: 878, 321, 116, 100
+right-rear-thigh
+	bounds: 647, 67, 91, 149
+right-wing07
+	bounds: 2, 369, 365, 643
+right-wing08
+	bounds: 369, 369, 365, 643
+right-wing09
+	bounds: 2, 2, 365, 643
+	rotate: 90
+tail02
+	bounds: 740, 67, 95, 120
+
+dragon_4.png
+	size: 1024, 1024
+	filter: Linear, Linear
+left-wing04
+	bounds: 2, 268, 264, 589
+left-wing05
+	bounds: 268, 268, 264, 589
+left-wing06
+	bounds: 534, 268, 264, 589
+left-wing07
+	bounds: 2, 2, 264, 589
+	rotate: 90
+
+dragon_5.png
+	size: 1024, 1024
+	filter: Linear, Linear
+left-wing08
+	bounds: 2, 2, 264, 589
+left-wing09
+	bounds: 268, 2, 264, 589

二进制
spine-ios/Example/Spine iOS Example/Assets/dragon/dragon.png


二进制
spine-ios/Example/Spine iOS Example/Assets/dragon/dragon_2.png


二进制
spine-ios/Example/Spine iOS Example/Assets/dragon/dragon_3.png


二进制
spine-ios/Example/Spine iOS Example/Assets/dragon/dragon_4.png


二进制
spine-ios/Example/Spine iOS Example/Assets/dragon/dragon_5.png


二进制
spine-ios/Example/Spine iOS Example/Assets/mixandmatch/mix-and-match-pro.skel


+ 358 - 0
spine-ios/Example/Spine iOS Example/Assets/mixandmatch/mix-and-match.atlas

@@ -0,0 +1,358 @@
+mix-and-match.png
+	size: 1024, 512
+	filter: Linear, Linear
+	scale: 0.5
+base-head
+	bounds: 118, 70, 95, 73
+boy/arm-front
+	bounds: 831, 311, 36, 115
+	rotate: 90
+boy/backpack
+	bounds: 249, 357, 119, 153
+boy/backpack-pocket
+	bounds: 628, 193, 34, 62
+	rotate: 90
+boy/backpack-strap-front
+	bounds: 330, 263, 38, 88
+	rotate: 90
+boy/backpack-up
+	bounds: 482, 171, 21, 70
+boy/body
+	bounds: 845, 413, 97, 132
+	rotate: 90
+boy/boot-ribbon-front
+	bounds: 234, 304, 9, 11
+boy/collar
+	bounds: 471, 243, 73, 29
+	rotate: 90
+boy/ear
+	bounds: 991, 352, 19, 23
+	rotate: 90
+boy/eye-back-low-eyelid
+	bounds: 66, 72, 17, 6
+boy/eye-back-pupil
+	bounds: 694, 279, 8, 9
+	rotate: 90
+boy/eye-back-up-eyelid
+	bounds: 460, 101, 23, 5
+	rotate: 90
+boy/eye-back-up-eyelid-back
+	bounds: 979, 414, 19, 10
+	rotate: 90
+boy/eye-front-low-eyelid
+	bounds: 1015, 203, 22, 7
+	rotate: 90
+boy/eye-front-pupil
+	bounds: 309, 50, 9, 9
+boy/eye-front-up-eyelid
+	bounds: 991, 373, 31, 6
+boy/eye-front-up-eyelid-back
+	bounds: 107, 76, 26, 9
+	rotate: 90
+boy/eye-iris-back
+	bounds: 810, 260, 17, 17
+boy/eye-iris-front
+	bounds: 902, 230, 18, 18
+boy/eye-white-back
+	bounds: 599, 179, 20, 12
+boy/eye-white-front
+	bounds: 544, 183, 27, 13
+boy/eyebrow-back
+	bounds: 1002, 225, 20, 11
+	rotate: 90
+boy/eyebrow-front
+	bounds: 975, 234, 25, 11
+boy/hair-back
+	bounds: 629, 289, 122, 81
+	rotate: 90
+boy/hair-bangs
+	bounds: 505, 180, 70, 37
+	rotate: 90
+boy/hair-side
+	bounds: 979, 435, 25, 43
+	rotate: 90
+boy/hand-backfingers
+	bounds: 858, 183, 19, 21
+boy/hand-front-fingers
+	bounds: 879, 183, 19, 21
+boy/hat
+	bounds: 218, 121, 93, 56
+boy/leg-front
+	bounds: 85, 104, 31, 158
+boy/mouth-close
+	bounds: 467, 100, 21, 5
+girl-blue-cape/mouth-close
+	bounds: 467, 100, 21, 5
+girl-spring-dress/mouth-close
+	bounds: 467, 100, 21, 5
+girl/mouth-close
+	bounds: 467, 100, 21, 5
+boy/mouth-smile
+	bounds: 1015, 258, 29, 7
+	rotate: 90
+boy/nose
+	bounds: 323, 79, 17, 10
+boy/pompom
+	bounds: 979, 462, 48, 43
+	rotate: 90
+boy/zip
+	bounds: 922, 231, 14, 23
+	rotate: 90
+girl-blue-cape/back-eyebrow
+	bounds: 527, 106, 18, 12
+	rotate: 90
+girl-blue-cape/body-dress
+	bounds: 2, 264, 109, 246
+girl-blue-cape/body-ribbon
+	bounds: 576, 193, 50, 38
+girl-blue-cape/cape-back
+	bounds: 113, 317, 134, 193
+girl-blue-cape/cape-back-up
+	bounds: 504, 305, 123, 106
+girl-blue-cape/cape-ribbon
+	bounds: 396, 118, 50, 18
+	rotate: 90
+girl-blue-cape/cape-shoulder-back
+	bounds: 420, 243, 49, 59
+girl-blue-cape/cape-shoulder-front
+	bounds: 2, 2, 62, 76
+girl-blue-cape/cape-up-front
+	bounds: 118, 145, 98, 117
+girl-blue-cape/ear
+	bounds: 837, 181, 19, 23
+girl-spring-dress/ear
+	bounds: 837, 181, 19, 23
+girl/ear
+	bounds: 837, 181, 19, 23
+girl-blue-cape/eye-back-low-eyelid
+	bounds: 810, 252, 17, 6
+girl-spring-dress/eye-back-low-eyelid
+	bounds: 810, 252, 17, 6
+girl/eye-back-low-eyelid
+	bounds: 810, 252, 17, 6
+girl-blue-cape/eye-back-pupil
+	bounds: 309, 40, 8, 9
+	rotate: 90
+girl-spring-dress/eye-back-pupil
+	bounds: 309, 40, 8, 9
+	rotate: 90
+girl/eye-back-pupil
+	bounds: 309, 40, 8, 9
+	rotate: 90
+girl-blue-cape/eye-back-up-eyelid
+	bounds: 573, 179, 24, 12
+girl-spring-dress/eye-back-up-eyelid
+	bounds: 573, 179, 24, 12
+girl/eye-back-up-eyelid
+	bounds: 573, 179, 24, 12
+girl-blue-cape/eye-back-up-eyelid-back
+	bounds: 380, 105, 17, 11
+	rotate: 90
+girl-spring-dress/eye-back-up-eyelid-back
+	bounds: 380, 105, 17, 11
+	rotate: 90
+girl/eye-back-up-eyelid-back
+	bounds: 380, 105, 17, 11
+	rotate: 90
+girl-blue-cape/eye-front-low-eyelid
+	bounds: 1016, 353, 18, 6
+	rotate: 90
+girl-spring-dress/eye-front-low-eyelid
+	bounds: 1016, 353, 18, 6
+	rotate: 90
+girl/eye-front-low-eyelid
+	bounds: 1016, 353, 18, 6
+	rotate: 90
+girl-blue-cape/eye-front-pupil
+	bounds: 363, 94, 9, 9
+girl-spring-dress/eye-front-pupil
+	bounds: 363, 94, 9, 9
+girl/eye-front-pupil
+	bounds: 363, 94, 9, 9
+girl-blue-cape/eye-front-up-eyelid
+	bounds: 679, 413, 30, 14
+	rotate: 90
+girl-spring-dress/eye-front-up-eyelid
+	bounds: 679, 413, 30, 14
+	rotate: 90
+girl/eye-front-up-eyelid
+	bounds: 679, 413, 30, 14
+	rotate: 90
+girl-blue-cape/eye-front-up-eyelid-back
+	bounds: 947, 234, 26, 11
+girl-spring-dress/eye-front-up-eyelid-back
+	bounds: 947, 234, 26, 11
+girl/eye-front-up-eyelid-back
+	bounds: 947, 234, 26, 11
+girl-blue-cape/eye-iris-back
+	bounds: 323, 105, 17, 17
+girl-blue-cape/eye-iris-front
+	bounds: 467, 107, 18, 18
+girl-blue-cape/eye-white-back
+	bounds: 621, 175, 20, 16
+girl-spring-dress/eye-white-back
+	bounds: 621, 175, 20, 16
+girl-blue-cape/eye-white-front
+	bounds: 643, 175, 20, 16
+girl-spring-dress/eye-white-front
+	bounds: 643, 175, 20, 16
+girl/eye-white-front
+	bounds: 643, 175, 20, 16
+girl-blue-cape/front-eyebrow
+	bounds: 309, 101, 18, 12
+	rotate: 90
+girl-blue-cape/hair-back
+	bounds: 712, 317, 117, 98
+girl-blue-cape/hair-bangs
+	bounds: 313, 170, 91, 40
+	rotate: 90
+girl-blue-cape/hair-head-side-back
+	bounds: 544, 198, 30, 52
+girl-blue-cape/hair-head-side-front
+	bounds: 466, 127, 41, 42
+girl-blue-cape/hair-side
+	bounds: 175, 2, 36, 71
+	rotate: 90
+girl-blue-cape/hand-front-fingers
+	bounds: 902, 207, 19, 21
+girl-spring-dress/hand-front-fingers
+	bounds: 902, 207, 19, 21
+girl-blue-cape/leg-front
+	bounds: 519, 413, 30, 158
+	rotate: 90
+girl-blue-cape/mouth-smile
+	bounds: 1015, 227, 29, 7
+	rotate: 90
+girl-spring-dress/mouth-smile
+	bounds: 1015, 227, 29, 7
+	rotate: 90
+girl/mouth-smile
+	bounds: 1015, 227, 29, 7
+	rotate: 90
+girl-blue-cape/nose
+	bounds: 342, 82, 11, 7
+girl-spring-dress/nose
+	bounds: 342, 82, 11, 7
+girl/nose
+	bounds: 342, 82, 11, 7
+girl-blue-cape/sleeve-back
+	bounds: 416, 95, 42, 29
+girl-blue-cape/sleeve-front
+	bounds: 249, 303, 52, 119
+	rotate: 90
+girl-spring-dress/arm-front
+	bounds: 829, 292, 17, 111
+	rotate: 90
+girl-spring-dress/back-eyebrow
+	bounds: 309, 81, 18, 12
+	rotate: 90
+girl-spring-dress/body-up
+	bounds: 66, 2, 64, 66
+girl-spring-dress/cloak-down
+	bounds: 758, 227, 50, 50
+girl-spring-dress/cloak-up
+	bounds: 628, 229, 64, 58
+girl-spring-dress/eye-iris-back
+	bounds: 342, 105, 17, 17
+girl-spring-dress/eye-iris-front
+	bounds: 487, 107, 18, 18
+girl-spring-dress/front-eyebrow
+	bounds: 323, 91, 18, 12
+girl-spring-dress/hair-back
+	bounds: 370, 417, 147, 93
+girl-spring-dress/hair-bangs
+	bounds: 829, 250, 91, 40
+girl-spring-dress/hair-head-side-back
+	bounds: 509, 126, 30, 52
+girl-spring-dress/hair-head-side-front
+	bounds: 816, 206, 41, 42
+girl-spring-dress/hair-side
+	bounds: 248, 2, 36, 71
+	rotate: 90
+girl-spring-dress/leg-front
+	bounds: 831, 381, 30, 158
+	rotate: 90
+girl-spring-dress/neck
+	bounds: 85, 70, 20, 32
+girl-spring-dress/shoulder-ribbon
+	bounds: 175, 44, 36, 24
+girl-spring-dress/skirt
+	bounds: 2, 80, 182, 81
+	rotate: 90
+girl-spring-dress/underskirt
+	bounds: 519, 445, 175, 65
+girl/arm-front
+	bounds: 712, 279, 36, 115
+	rotate: 90
+girl/back-eyebrow
+	bounds: 309, 61, 18, 12
+	rotate: 90
+girl/bag-base
+	bounds: 694, 219, 62, 58
+girl/bag-strap-front
+	bounds: 370, 304, 12, 96
+	rotate: 90
+girl/bag-top
+	bounds: 765, 175, 49, 50
+girl/body
+	bounds: 370, 318, 97, 132
+	rotate: 90
+girl/boot-ribbon-front
+	bounds: 323, 64, 13, 13
+girl/eye-iris-back
+	bounds: 361, 105, 17, 17
+girl/eye-iris-front
+	bounds: 507, 106, 18, 18
+girl/eye-white-back
+	bounds: 665, 175, 20, 16
+girl/front-eyebrow
+	bounds: 343, 91, 18, 12
+girl/hair-back
+	bounds: 696, 417, 147, 93
+girl/hair-bangs
+	bounds: 922, 247, 91, 40
+girl/hair-flap-down-front
+	bounds: 415, 171, 70, 65
+	rotate: 90
+girl/hair-head-side-back
+	bounds: 991, 381, 30, 52
+girl/hair-head-side-front
+	bounds: 859, 206, 41, 42
+girl/hair-patch
+	bounds: 132, 2, 66, 41
+	rotate: 90
+girl/hair-side
+	bounds: 692, 181, 36, 71
+	rotate: 90
+girl/hair-strand-back-1
+	bounds: 948, 289, 58, 74
+	rotate: 90
+girl/hair-strand-back-2
+	bounds: 355, 170, 91, 58
+	rotate: 90
+girl/hair-strand-back-3
+	bounds: 215, 40, 92, 79
+girl/hair-strand-front-1
+	bounds: 234, 263, 38, 94
+	rotate: 90
+girl/hair-strand-front-2
+	bounds: 576, 233, 70, 50
+	rotate: 90
+girl/hair-strand-front-3
+	bounds: 313, 124, 44, 81
+	rotate: 90
+girl/hand-front-fingers
+	bounds: 923, 208, 19, 21
+girl/hat
+	bounds: 218, 179, 93, 82
+girl/leg-front
+	bounds: 831, 349, 30, 158
+	rotate: 90
+girl/pompom
+	bounds: 416, 126, 48, 43
+girl/scarf
+	bounds: 113, 264, 119, 51
+girl/scarf-back
+	bounds: 502, 252, 72, 51
+girl/zip
+	bounds: 816, 179, 19, 25

二进制
spine-ios/Example/Spine iOS Example/Assets/mixandmatch/mix-and-match.png


文件差异内容过多而无法显示
+ 557 - 0
spine-ios/Example/Spine iOS Example/Assets/spineboy/spineboy-pro.json


二进制
spine-ios/Example/Spine iOS Example/Assets/spineboy/spineboy-pro.skel


+ 94 - 0
spine-ios/Example/Spine iOS Example/Assets/spineboy/spineboy.atlas

@@ -0,0 +1,94 @@
+spineboy.png
+	size: 1024, 256
+	filter: Linear, Linear
+	scale: 0.5
+crosshair
+	bounds: 352, 7, 45, 45
+eye-indifferent
+	bounds: 862, 105, 47, 45
+eye-surprised
+	bounds: 505, 79, 47, 45
+front-bracer
+	bounds: 826, 66, 29, 40
+front-fist-closed
+	bounds: 786, 65, 38, 41
+front-fist-open
+	bounds: 710, 51, 43, 44
+	rotate: 90
+front-foot
+	bounds: 210, 6, 63, 35
+front-shin
+	bounds: 665, 128, 41, 92
+	rotate: 90
+front-thigh
+	bounds: 2, 2, 23, 56
+	rotate: 90
+front-upper-arm
+	bounds: 250, 205, 23, 49
+goggles
+	bounds: 665, 171, 131, 83
+gun
+	bounds: 798, 152, 105, 102
+head
+	bounds: 2, 27, 136, 149
+hoverboard-board
+	bounds: 2, 178, 246, 76
+hoverboard-thruster
+	bounds: 722, 96, 30, 32
+	rotate: 90
+hoverglow-small
+	bounds: 275, 81, 137, 38
+mouth-grind
+	bounds: 614, 97, 47, 30
+mouth-oooo
+	bounds: 612, 65, 47, 30
+mouth-smile
+	bounds: 661, 64, 47, 30
+muzzle-glow
+	bounds: 382, 54, 25, 25
+muzzle-ring
+	bounds: 275, 54, 25, 105
+	rotate: 90
+muzzle01
+	bounds: 911, 95, 67, 40
+	rotate: 90
+muzzle02
+	bounds: 792, 108, 68, 42
+muzzle03
+	bounds: 956, 171, 83, 53
+	rotate: 90
+muzzle04
+	bounds: 275, 7, 75, 45
+muzzle05
+	bounds: 140, 3, 68, 38
+neck
+	bounds: 250, 182, 18, 21
+portal-bg
+	bounds: 140, 43, 133, 133
+portal-flare1
+	bounds: 554, 65, 56, 30
+portal-flare2
+	bounds: 759, 112, 57, 31
+	rotate: 90
+portal-flare3
+	bounds: 554, 97, 58, 30
+portal-shade
+	bounds: 275, 121, 133, 133
+portal-streaks1
+	bounds: 410, 126, 126, 128
+portal-streaks2
+	bounds: 538, 129, 125, 125
+rear-bracer
+	bounds: 857, 67, 28, 36
+rear-foot
+	bounds: 663, 96, 57, 30
+rear-shin
+	bounds: 414, 86, 38, 89
+	rotate: 90
+rear-thigh
+	bounds: 756, 63, 28, 47
+rear-upper-arm
+	bounds: 60, 5, 20, 44
+	rotate: 90
+torso
+	bounds: 905, 164, 49, 90

二进制
spine-ios/Example/Spine iOS Example/Assets/spineboy/spineboy.png


+ 76 - 0
spine-ios/Example/Spine iOS Example/DebugRendering.swift

@@ -0,0 +1,76 @@
+import SwiftUI
+import Spine
+
+struct DebugRendering: View {
+    
+    @StateObject
+    var model = DebugRenderingModel()
+    
+    var body: some View {
+        ZStack {
+            Color.red.ignoresSafeArea()
+            SpineView(
+                from: .bundle(atlasFileName: "spineboy.atlas", skeletonFileName: "spineboy-pro.skel"),
+                controller: model.controller,
+                mode: .fit,
+                alignment: .center
+            )
+            ForEach(model.boneRects, id: \.id) { boneLocation in
+                Rectangle()
+                    .fill(.blue)
+                    .offset(x: boneLocation.x, y: boneLocation.y)
+                    .frame(width: boneLocation.width, height: boneLocation.height)
+            }
+        }
+        .navigationTitle("Debug Rendering")
+        .navigationBarTitleDisplayMode(.inline)
+    }
+}
+
+#Preview {
+    DebugRendering()
+}
+
+final class DebugRenderingModel: ObservableObject {
+    
+    @Published
+    var controller: SpineController!
+    
+    @Published
+    var boneRects = [BoneRect]()
+    
+    init() {
+        controller = SpineController(
+            onInitialized: { controller in
+                controller.animationState.setAnimationByName(
+                    trackIndex: 0,
+                    animationName: "walk",
+                    loop: true
+                )
+            },
+            onAfterPaint: { 
+                [weak self] controller in guard let self else { return }
+                boneRects = controller.drawable.skeleton.bones.map { bone in
+                    let position = controller.fromSkeletonCoordinates(
+                        position: CGPointMake(CGFloat(bone.worldX), CGFloat(bone.worldY))
+                    )
+                    return BoneRect(
+                        id: UUID(),
+                        x: position.x,
+                        y: position.y,
+                        width: 5,
+                        height: 5
+                    )
+                }
+            }
+        )
+    }
+}
+
+struct BoneRect: Hashable {
+    let id: UUID
+    let x: CGFloat
+    let y: CGFloat
+    let width: CGFloat
+    let height: CGFloat
+}

+ 61 - 0
spine-ios/Example/Spine iOS Example/DisableRendering.swift

@@ -0,0 +1,61 @@
+import SwiftUI
+import Spine
+
+struct DisableRendering: View {
+    
+    @StateObject
+    var controller = SpineController(
+        onInitialized: { controller in
+            controller.animationState.setAnimationByName(
+                trackIndex: 0,
+                animationName: "walk",
+                loop: true
+            )
+        }
+    )
+    
+    @State
+    var isRendering: Bool?
+    
+    var body: some View {
+        VStack {
+            List {
+                VStack(alignment: .leading) {
+                    Text("Scroll spine boy out of the viewport")
+                    Text("Rendering is disabled when the spine view moves out of the viewport, preserving CPU/GPU resources.")
+                        .foregroundColor(.secondary)
+                }
+                
+                SpineView(
+                    from: .bundle(atlasFileName: "spineboy.atlas", skeletonFileName: "spineboy-pro.skel"),
+                    controller: controller,
+                    isRendering: $isRendering
+                )
+                .frame(minHeight: 200)
+                .onAppear {
+                    isRendering = true
+                    print("rendering enabled")
+                }
+                .onDisappear {
+                    isRendering = false
+                    print("rendering disabled")
+                }
+                
+                Text("Foo")
+                    .frame(minHeight: 400)
+                
+                Text("Bar")
+                    .frame(minHeight: 400)
+                
+                Text("Baz")
+                    .frame(minHeight: 400)
+            }
+        }
+        .navigationTitle("Disable Rendering")
+        .navigationBarTitleDisplayMode(.inline)
+    }
+}
+
+#Preview {
+    DisableRendering()
+}

+ 129 - 0
spine-ios/Example/Spine iOS Example/DressUp.swift

@@ -0,0 +1,129 @@
+import SwiftUI
+import Spine
+import SpineCppLite
+
+struct DressUp: View {
+    
+    @StateObject
+    var model = DressUpModel()
+    
+    var body: some View {
+        HStack(spacing: 0) {
+            List {
+                ForEach(model.skinImages.keys.sorted(), id: \.self) { skinName in
+                    let rawImageData = model.skinImages[skinName]!
+                    Button(action: { model.toggleSkin(skinName: skinName) }) {
+                        Image(uiImage: UIImage(cgImage: rawImageData))
+                            .resizable()
+                            .scaledToFit()
+                            .frame(width: model.thumbnailSize.width, height: model.thumbnailSize.height)
+                            .grayscale(model.selectedSkins[skinName] == true ? 0.0 : 1.0)
+                    }
+                }
+            }
+            .listStyle(.plain)
+            
+            Divider()
+            
+            if let drawable = model.drawable {
+                SpineView(
+                    from: .drawable(drawable),
+                    controller: model.controller,
+                    boundsProvider: SkinAndAnimationBounds(skins: ["full-skins/girl"])
+                )
+            } else {
+                Spacer()
+            }
+        }
+        .navigationTitle("Dress Up")
+        .navigationBarTitleDisplayMode(.inline)
+    }
+}
+
+#Preview {
+    DressUp()
+}
+
+final class DressUpModel: ObservableObject {
+    
+    let thumbnailSize = CGSize(width: 200, height: 200)
+    
+    @Published
+    var controller: SpineController
+    
+    @Published
+    var drawable: SkeletonDrawableWrapper?
+    
+    @Published
+    var skinImages = [String: CGImage]()
+    
+    @Published
+    var selectedSkins = [String: Bool]()
+    
+    private var customSkin: Skin?
+    
+    init() {
+        controller = SpineController(
+            onInitialized: { controller in
+                controller.animationState.setAnimationByName(
+                    trackIndex: 0,
+                    animationName: "dance",
+                    loop: true
+                )
+            },
+            disposeDrawableOnDeInit: false
+        )
+        Task.detached(priority: .high) {
+            let drawable = try await SkeletonDrawableWrapper.fromBundle(
+                atlasFileName: "mix-and-match.atlas",
+                skeletonFileName: "mix-and-match-pro.skel"
+            )
+            try await MainActor.run {
+                for skin in drawable.skeletonData.skins {
+                    if skin.name == "default" { continue }
+                    let skeleton = drawable.skeleton
+                    skeleton.skin = skin
+                    skeleton.setToSetupPose()
+                    skeleton.update(delta: 0)
+                    skeleton.updateWorldTransform(physics: SPINE_PHYSICS_UPDATE)
+                    try skin.name.flatMap { skinName in
+                        self.skinImages[skinName] = try drawable.renderToImage(
+                            size: self.thumbnailSize,
+                            backgroundColor: .white,
+                            scaleFactor: UIScreen.main.scale
+                        )
+                        self.selectedSkins[skinName] = false
+                    }
+                }
+                self.toggleSkin(skinName: "full-skins/girl", drawable: drawable)
+                self.drawable = drawable
+            }
+        }
+    }
+    
+    deinit {
+        drawable?.dispose()
+        customSkin?.dispose()
+    }
+    
+    func toggleSkin(skinName: String) {
+        if let drawable {
+            toggleSkin(skinName: skinName, drawable: drawable)
+        }
+    }
+    
+    func toggleSkin(skinName: String, drawable: SkeletonDrawableWrapper) {
+        selectedSkins[skinName] = !(selectedSkins[skinName] ?? false)
+        customSkin?.dispose()
+        customSkin = Skin.create(name: "custom-skin")
+        for skinName in selectedSkins.keys {
+          if selectedSkins[skinName] == true {
+              if let skin = drawable.skeletonData.findSkin(name: skinName) {
+                  customSkin?.addSkin(other: skin)
+              }
+          }
+        }
+        drawable.skeleton.skin = customSkin
+        drawable.skeleton.setToSetupPose()
+    }
+}

+ 73 - 0
spine-ios/Example/Spine iOS Example/IKFollowing.swift

@@ -0,0 +1,73 @@
+import SwiftUI
+import Spine
+
+struct IKFollowing: View {
+    
+    @StateObject
+    var model = IKFollowingModel()
+        
+    var body: some View {
+        SpineView(
+            from: .bundle(atlasFileName: "spineboy.atlas", skeletonFileName: "spineboy-pro.skel"),
+            controller: model.controller,
+            alignment: .centerLeft
+        )
+        .gesture(
+            DragGesture(minimumDistance: 0)
+                .onChanged { gesture in
+                    model.crossHairPosition = model.controller.toSkeletonCoordinates(
+                        position: gesture.location
+                    )
+                }
+        )
+        .navigationTitle("IK Following")
+        .navigationBarTitleDisplayMode(.inline)
+    }
+}
+
+#Preview {
+    if #available(iOS 15.0, *) {
+        IKFollowing()
+            .previewInterfaceOrientation(.landscapeLeft)
+    } else {
+        IKFollowing()
+    }
+}
+
+final class IKFollowingModel: ObservableObject {
+    
+    @Published
+    var controller: SpineController!
+    
+    @Published
+    var crossHairPosition: CGPoint?
+    
+    init() {
+        controller = SpineController(
+            onInitialized: { controller in
+                controller.animationState.setAnimationByName(
+                    trackIndex: 0,
+                    animationName: "walk",
+                    loop: true
+                )
+                controller.animationState.setAnimationByName(
+                    trackIndex: 1,
+                    animationName: "aim",
+                    loop: true
+                )
+            },
+            onAfterUpdateWorldTransforms: { 
+                [weak self] controller in guard let self else { return }
+                guard let worldPosition = self.crossHairPosition else {
+                    return
+                }
+                let bone = controller.skeleton.findBone(boneName: "crosshair")!
+                if let parent = bone.parent {
+                    let position = parent.worldToLocal(worldX: Float(worldPosition.x), worldY:  Float(worldPosition.y))
+                    bone.x = position.x
+                    bone.y = position.y
+                }
+            }
+        )
+    }
+}

+ 61 - 0
spine-ios/Example/Spine iOS Example/MainView.swift

@@ -0,0 +1,61 @@
+import SwiftUI
+import Spine
+
+struct MainView: View {
+    var body: some View {
+        List {
+            Section {
+                NavigationLink("Simple Animation") {
+                    SimpleAnimation()
+                }
+                NavigationLink("Play/Pause") {
+                    PlayPauseAnimation()
+                }
+                NavigationLink("Animation State Listener") {
+                    AnimationStateEvents()
+                }
+                NavigationLink("Debug Rendering") {
+                    DebugRendering()
+                }
+                NavigationLink("Dress Up") {
+                    DressUp()
+                }
+                NavigationLink("IK Following") {
+                    IKFollowing()
+                }
+                NavigationLink("Physics") {
+                    Physics()
+                }
+                NavigationLink("Disable Rendering") {
+                    DisableRendering()
+                }
+            } header: {
+                Text("Swift + SwiftUI")
+            }
+            Section {
+                NavigationLink("Simple Animation") {
+                    SimpleAnimationViewControllerRepresentable()
+                        .navigationTitle("Simple Animation")
+                        .navigationBarTitleDisplayMode(.inline)
+                }
+            } header: {
+                Text("ObjC + UIKit")
+            } footer: {
+                HStack {
+                    Spacer()
+                    Text("Spine \(Spine.version)")
+                        .font(.footnote)
+                        .foregroundColor(.secondary)
+                    Spacer()
+                }
+            }
+        }
+        .navigationTitle("Spine Examples")
+    }
+}
+
+#Preview {
+    NavigationView {
+        MainView()
+    }
+}

+ 77 - 0
spine-ios/Example/Spine iOS Example/Physics.swift

@@ -0,0 +1,77 @@
+import SwiftUI
+import Spine
+
+struct Physics: View {
+    
+    @StateObject
+    var model = PhysicsModel()
+    
+    var body: some View {
+        SpineView(
+            from: .bundle(atlasFileName: "celestial-circus.atlas", skeletonFileName: "celestial-circus-pro.skel"),
+            controller: model.controller
+        )
+        .gesture(
+            DragGesture(minimumDistance: 0)
+                .onChanged { gesture in
+                    model.updateBonePosition(position: gesture.location)
+                }
+        )
+        .navigationTitle("Physics (drag anywhere)")
+        .navigationBarTitleDisplayMode(.inline)
+    }
+}
+
+#Preview {
+    Physics()
+}
+
+final class PhysicsModel: ObservableObject {
+    
+    @Published
+    var controller: SpineController!
+    
+    @Published
+    var mousePosition: CGPoint?
+    
+    @Published
+    var lastMousePosition: CGPoint?
+    
+    init() {
+        controller = SpineController(
+            onInitialized: { controller in
+                controller.animationState.setAnimationByName(
+                    trackIndex: 0,
+                    animationName: "eyeblink-long",
+                    loop: true
+                )
+                controller.animationState.setAnimationByName(
+                    trackIndex: 0,
+                    animationName: "wings-and-feet",
+                    loop: true
+                )
+            },
+            onAfterUpdateWorldTransforms: {
+                [weak self] controller in guard let self else { return }
+                
+                guard let lastMousePosition else {
+                    self.lastMousePosition = mousePosition
+                    return
+                }
+                guard let mousePosition else {
+                    return
+                }
+                let dx = mousePosition.x - lastMousePosition.x
+                let dy = mousePosition.y - lastMousePosition.y
+                let positionX = controller.skeleton.x + Float(dx)
+                let positionY = controller.skeleton.y + Float(dy)
+                controller.skeleton.setPosition(x: positionX, y: positionY)
+                self.lastMousePosition = mousePosition
+            }
+        )
+    }
+    
+    func updateBonePosition(position: CGPoint) {
+        mousePosition = controller.toSkeletonCoordinates(position: position)
+    }
+}

+ 45 - 0
spine-ios/Example/Spine iOS Example/PlayPauseAnimation.swift

@@ -0,0 +1,45 @@
+import SwiftUI
+import Spine
+
+struct PlayPauseAnimation: View {
+    
+    @StateObject
+    var controller = SpineController(
+        onInitialized: { controller in
+            controller.animationState.setAnimationByName(
+                trackIndex: 0,
+                animationName: "flying",
+                loop: true
+            )
+        }
+    )
+    
+    var body: some View {
+        SpineView(
+            from: .bundle(atlasFileName: "dragon.atlas", skeletonFileName: "dragon-ess.skel"),
+//            from: .http(
+//                atlasURL: URL(string: "https://github.com/denrase/spine-runtimes/raw/spine-ios/spine-ios/Example/Spine%20iOS%20Example/Assets/dragon/dragon.atlas")!,
+//                skeletonURL:  URL(string: "https://github.com/denrase/spine-runtimes/raw/spine-ios/spine-ios/Example/Spine%20iOS%20Example/Assets/dragon/dragon-ess.skel")!
+//            ),
+            controller: controller,
+            boundsProvider: SkinAndAnimationBounds(animation: "flying")
+        )
+        .navigationTitle("Play/Pause")
+        .navigationBarTitleDisplayMode(.inline)
+        .toolbar {
+            Button(action: {
+                if controller.isPlaying {
+                    controller.pause()
+                } else {
+                    controller.resume()
+                }
+            }) {
+                Image(systemName: controller.isPlaying ? "pause.fill" : "play.fill")
+            }
+        }
+    }
+}
+
+#Preview {
+    PlayPauseAnimation()
+}

+ 35 - 0
spine-ios/Example/Spine iOS Example/SimpleAnimation.swift

@@ -0,0 +1,35 @@
+import SwiftUI
+import Spine
+
+struct SimpleAnimation: View {
+    
+    @StateObject
+    var controller = SpineController(
+        onInitialized: { controller in
+            controller.animationState.setAnimationByName(
+                trackIndex: 0,
+                animationName: "walk",
+                loop: true
+            )
+        }
+    )
+    
+    var body: some View {
+        SpineView(
+            from: .bundle(atlasFileName: "spineboy.atlas", skeletonFileName: "spineboy-pro.skel"),
+//            from: .http(
+//                atlasURL: URL(string: "https://github.com/denrase/spine-runtimes/raw/spine-ios/spine-ios/Example/Spine%20iOS%20Example/Assets/spineboy/spineboy.atlas")!,
+//                skeletonURL:  URL(string: "https://github.com/denrase/spine-runtimes/raw/spine-ios/spine-ios/Example/Spine%20iOS%20Example/Assets/spineboy/spineboy-pro.skel")!
+//            ),
+            controller: controller,
+            mode: .fit,
+            alignment: .center
+        )
+        .navigationTitle("Simple Animation")
+        .navigationBarTitleDisplayMode(.inline)
+    }
+}
+
+#Preview {
+    SimpleAnimation()
+}

+ 9 - 0
spine-ios/Example/Spine iOS Example/SimpleAnimationViewController.h

@@ -0,0 +1,9 @@
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface SimpleAnimationViewController : UIViewController
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 44 - 0
spine-ios/Example/Spine iOS Example/SimpleAnimationViewController.m

@@ -0,0 +1,44 @@
+#import "SimpleAnimationViewController.h"
+@import Spine;
+
+@interface SimpleAnimationViewController ()
+
+@property (nonatomic, strong) SpineController *spineController;
+
+@end
+
+@implementation SimpleAnimationViewController
+
+- (instancetype)init {
+    self = [super init];
+    if (self) {
+        self.spineController = [[SpineController alloc] initOnInitialized:^(SpineController *controller) {
+            [controller.animationState setAnimationByNameWithTrackIndex:0 animationName:@"walk" loop:YES];
+        }
+                                            onBeforeUpdateWorldTransforms:nil
+                                             onAfterUpdateWorldTransforms:nil
+                                                            onBeforePaint:nil
+                                                             onAfterPaint:nil
+                                                  disposeDrawableOnDeInit:YES];
+    }
+    return self;
+}
+
+- (void)viewDidLoad {
+    [super viewDidLoad];
+    
+    SpineUIView *spineView = [[SpineUIView alloc] initWithAtlasFileName:@"spineboy.atlas"
+                                                       skeletonFileName:@"spineboy-pro.skel"
+                                                                 bundle:[NSBundle mainBundle]
+                                                             controller:self.spineController
+                                                                   mode:ContentModeFit
+                                                              alignment:AlignmentCenter
+                                                         boundsProvider:[[SpineSetupPoseBounds alloc] init]
+                                                        backgroundColor:[UIColor clearColor]];
+    spineView.frame = self.view.bounds;
+    spineView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+    
+    [self.view addSubview:spineView];
+}
+
+@end

+ 13 - 0
spine-ios/Example/Spine iOS Example/SimpleAnimationViewControllerRepresentable.swift

@@ -0,0 +1,13 @@
+import SwiftUI
+
+struct SimpleAnimationViewControllerRepresentable: UIViewControllerRepresentable {
+    typealias UIViewControllerType = SimpleAnimationViewController
+    
+    func makeUIViewController(context: Context) -> SimpleAnimationViewController {
+        return SimpleAnimationViewController()
+    }
+    
+    func updateUIViewController(_ uiViewController: SimpleAnimationViewController, context: Context) {
+        //
+    }
+}

+ 5 - 0
spine-ios/Example/Spine iOS Example/Spine iOS Example-Bridging-Header.h

@@ -0,0 +1,5 @@
+//
+//  Use this file to import your target's public headers that you would like to expose to Swift.
+//
+
+#import "SimpleAnimationViewController.h"

+ 14 - 0
spine-ios/Example/Spine iOS Example/SpineExampleApp.swift

@@ -0,0 +1,14 @@
+import SwiftUI
+import Spine
+
+@main
+struct SpineExampleApp: App {
+    
+    var body: some Scene {
+        WindowGroup {
+            NavigationView {
+                MainView()
+            }
+        }
+    }
+}

+ 10 - 0
spine-ios/Example/Spine iOS Example/SpineiOSExample.entitlements

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+    <key>com.apple.security.app-sandbox</key>
+    <true/>
+    <key>com.apple.security.files.user-selected.read-only</key>
+    <true/>
+</dict>
+</plist>

+ 6 - 0
spine-ios/README.md

@@ -0,0 +1,6 @@
+# spine-ios
+
+## Setup
+
+- Go to "Build Settings"
+- Set "C++ and Objective-C Interoperability" to "C++ / Objective-C++"

+ 70 - 0
spine-ios/Sources/Spine/AnimationStateWrapper.swift

@@ -0,0 +1,70 @@
+import Foundation
+import SpineCppLite
+
+public typealias AnimationStateListener = (_ type: EventType, _ entry: TrackEntry, _ event: Event?) -> Void
+
+/// Wrapper class around ``AnimationState``. Applies animations over time, queues animations for later playback, mixes (crossfading) between animations, and applies
+/// multiple animations on top of each other (layering).
+///
+/// See [Applying Animations](http://esotericsoftware.com/spine-applying-animations/) in the Spine Runtimes Guide.
+@objc(SpineAnimationStateWrapper)
+@objcMembers
+public final class AnimationStateWrapper: NSObject {
+    
+    public let animationState: AnimationState
+    public let aninationStateEvents: AnimationStateEvents
+    
+    private var trackEntryListeners = [spine_track_entry: AnimationStateListener]()
+    
+    private var stateListener: AnimationStateListener?
+    
+    public init(animationState: AnimationState, aninationStateEvents: AnimationStateEvents) {
+        self.animationState = animationState
+        self.aninationStateEvents = aninationStateEvents
+        super.init()
+    }
+    
+    /// The listener for events generated by the provided ``TrackEntry``, or nil.
+    ///
+    /// A track entry returned from ``AnimationState/setAnimation(trackIndex:animation:loop:)`` is already the current animation
+    /// for the track, so the track entry listener will not be called for ``EventType`` `SPINE_EVENT_TYPE_START`.
+    public func setTrackEntryListener(entry: TrackEntry, listener: AnimationStateListener?) {
+        if let listener {
+            trackEntryListeners[entry.wrappee] = listener
+        } else {
+            trackEntryListeners.removeValue(forKey: entry.wrappee)
+        }
+    }
+    
+    /// Increments each track entry ``TrackEntry/trackTime``, setting queued animations as current if needed.
+    public func update(delta: Float) {
+        animationState.update(delta: delta)
+        
+        let numEvents = spine_animation_state_events_get_num_events(aninationStateEvents.wrappee)
+        for i in 0..<numEvents {
+            let type = aninationStateEvents.getEventType(index: i)
+            
+            let entry = aninationStateEvents.getTrackEntry(index: i)
+            let event = aninationStateEvents.getEvent(index: i)
+            
+            if let trackEntryListener = trackEntryListeners[entry.wrappee] {
+                trackEntryListener(type, entry, event)
+            }
+            if let stateListener {
+                stateListener(type, entry, event)
+            }
+            if type == SPINE_EVENT_TYPE_DISPOSE {
+                spine_animation_state_dispose_track_entry(animationState.wrappee, entry.wrappee)
+            }
+        }
+        aninationStateEvents.reset()
+    }
+    
+    /// The listener for events generated for all tracks managed by the ``AnimationState``, or nil.
+    ///
+    /// A track entry returned from ``AnimationState/setAnimation(trackIndex:animation:loop:)`` is already the current animation
+    /// for the track, so the track entry listener will not be called for ``EventType`` `SPINE_EVENT_TYPE_START`.
+    public func setStateListener(_ stateListener: AnimationStateListener?) {
+        self.stateListener = stateListener
+    }
+}

+ 157 - 0
spine-ios/Sources/Spine/BoundsProvider.swift

@@ -0,0 +1,157 @@
+import Foundation
+import CoreGraphics
+
+/// Base class for bounds providers. A bounds provider calculates the axis aligned bounding box
+/// used to scale and fit a skeleton inside the bounds of a ``SpineUIView``.
+@objc(SpineBoundsProvider)
+public protocol BoundsProvider {
+    func computeBounds(for drawable: SkeletonDrawableWrapper) -> CGRect
+}
+
+/// A ``BoundsProvider`` that calculates the bounding box of the skeleton based on the visible
+/// attachments in the setup pose.
+@objc(SpineSetupPoseBounds)
+@objcMembers
+public final class SetupPoseBounds: NSObject, BoundsProvider {
+    
+    public override init() {
+        super.init()
+    }
+
+    public func computeBounds(for drawable: SkeletonDrawableWrapper) -> CGRect {
+        return CGRect(bounds: drawable.skeleton.bounds)
+    }
+}
+
+/// A ``BoundsProvider`` that returns fixed bounds.
+@objc(SpineRawBounds)
+@objcMembers
+public final class RawBounds: NSObject, BoundsProvider {
+    public let x: Double
+    public let y: Double
+    public let width: Double
+    public let height: Double
+
+    public init(x: Double, y: Double, width: Double, height: Double) {
+        self.x = x
+        self.y = y
+        self.width = width
+        self.height = height
+        super.init()
+    }
+
+    public func computeBounds(for drawable: SkeletonDrawableWrapper) -> CGRect {
+        return CGRectMake(CGFloat(x), CGFloat(y), CGFloat(width), CGFloat(height))
+    }
+}
+
+/// A ``BoundsProvider`` that calculates the bounding box needed for a combination of skins
+/// and an animation.
+@objc(SpineSkinAndAnimationBounds)
+@objcMembers
+public final class SkinAndAnimationBounds: NSObject, BoundsProvider {
+    
+    private let animation: String?
+    private let skins: [String]
+    private let stepTime: TimeInterval;
+
+    /// Constructs a new provider that will use the given `skins` and `animation` to calculate
+    /// 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
+    /// across the entire animation.
+    public init(animation: String? = nil, skins: [String]? = nil, let stepTime: TimeInterval = 0.1) {
+        self.animation = animation
+        if let skins, !skins.isEmpty {
+            self.skins = skins
+        } else {
+            self.skins = ["default"]
+        }
+        self.stepTime = stepTime
+        super.init()
+    }
+    
+    public func computeBounds(for drawable: SkeletonDrawableWrapper) -> CGRect {
+        let data = drawable.skeletonData
+        let oldSkin: Skin? = drawable.skeleton.skin
+        let customSkin = Skin.create(name: "custom-skin")
+        for skinName in skins {
+            let skin = data.findSkin(name: skinName)
+            if let skin = data.findSkin(name: skinName) {
+                customSkin.addSkin(other: skin)
+            }
+        }
+        drawable.skeleton.skin = customSkin
+        drawable.skeleton.setToSetupPose();
+
+        let animation = animation.flatMap { data.findAnimation(name: $0) }
+        var minX = Float.Magnitude.greatestFiniteMagnitude
+        var minY = Float.Magnitude.greatestFiniteMagnitude
+        var maxX = -Float.Magnitude.greatestFiniteMagnitude
+        var maxY = -Float.Magnitude.greatestFiniteMagnitude
+        if let animation {
+            drawable.animationState.setAnimation(trackIndex: 0, animation: animation, loop: false)
+            let steps = Int(max(Double(animation.duration) / stepTime, 1.0))
+            for i in 0..<steps {
+                drawable.update(delta: i > 0 ? Float(stepTime) : 0.0)
+                let bounds = drawable.skeleton.bounds;
+                minX = min(minX, bounds.x)
+                minY = min(minY, bounds.y)
+                maxX = max(maxX, minX + bounds.width)
+                maxY = max(maxY, minY + bounds.height)
+            }
+        } else {
+            let bounds = drawable.skeleton.bounds;
+            minX = bounds.x
+            minY = bounds.y
+            maxX = minX + bounds.width
+            maxY = minY + bounds.height
+        }
+        drawable.skeleton.setSkinByName(skinName: "default")
+        drawable.animationState.clearTracks()
+        
+        if let oldSkin {
+            drawable.skeleton.skin = oldSkin
+        }
+        drawable.skeleton.setToSetupPose()
+        drawable.update(delta: 0)
+        customSkin.dispose()
+        return CGRectMake(CGFloat(minX), CGFloat(minY), CGFloat(maxX - minX), CGFloat(maxY - minY))
+      }
+}
+
+/// How a view should be inscribed into another view.
+@objc
+public enum ContentMode: Int {
+    case fit /// As large as possible while still containing the source view entirely within the target view.
+    case fill /// Fill the target view by distorting the source's aspect ratio.
+}
+
+/// How a view should aligned withing another view.
+@objc
+public enum Alignment: Int {
+    case topLeft
+    case topCenter
+    case topRight
+    case centerLeft
+    case center
+    case centerRight
+    case bottomLeft
+    case bottomCenter
+    case bottomRight
+    
+    internal var x: CGFloat {
+        switch self {
+        case .topLeft, .centerLeft, .bottomLeft: return -1.0
+        case .topCenter, .center, .bottomCenter: return 0.0
+        case .topRight, .centerRight, .bottomRight: return 1.0
+        }
+    }
+    
+    internal var y: CGFloat {
+        switch self {
+        case .topLeft, .topCenter, .topRight: return -1.0
+        case .centerLeft, .center, .centerRight: return 0.0
+        case .bottomLeft, .bottomCenter, .bottomRight: return -1.0
+        }
+    }
+}

+ 15 - 0
spine-ios/Sources/Spine/Extensions/MTLClearColor+UIColor.swift

@@ -0,0 +1,15 @@
+import UIKit
+import MetalKit
+
+extension MTLClearColor {
+    init(_ color: UIColor) {
+        var red: CGFloat = 0
+        var green: CGFloat = 0
+        var blue: CGFloat = 0
+        var alpha: CGFloat = 0
+        
+        color.getRed(&red, green: &green, blue: &blue, alpha: &alpha)
+        
+        self.init(red: Double(red), green: Double(green), blue: Double(blue), alpha: Double(alpha))
+    }
+}

+ 48 - 0
spine-ios/Sources/Spine/Extensions/RenderCommand+Vertices.swift

@@ -0,0 +1,48 @@
+import SpineShadersStructs
+import Foundation
+
+extension RenderCommand {
+    func getVertices() -> [SpineVertex] {
+        var vertices = [SpineVertex]()
+        
+        let indices = indices
+        let numVertices = numVertices
+        let positions = positions(numVertices: numVertices)
+        let uvs = uvs(numVertices: numVertices)
+        let colors = colors(numVertices: numVertices)
+        
+        for i in 0..<indices.count {
+            let index = Int(indices[i])
+            
+            let xIndex = 2 * index
+            let yIndex = xIndex + 1
+            
+            let positionX = positions[xIndex]
+            let positionY = positions[yIndex]
+            let uvX = uvs[xIndex]
+            let uvY = uvs[yIndex]
+            let color = extractRGBA(from: colors[index])
+            
+            let vertex = SpineVertex(
+                position: vector_float2(positionX, positionY),
+                color: color,
+                uv: vector_float2(uvX, uvY)
+            )
+            vertices.append(vertex)
+        }
+        
+        return vertices
+    }
+    
+    private func extractRGBA(from color: Int32) -> vector_float4 {
+        guard color != -1 else {
+            return vector_float4(1.0, 1.0, 1.0, 1.0)
+        }
+        let alpha = (color >> 24) & 0xFF
+        let red = (color >> 16) & 0xFF
+        let green = (color >> 8) & 0xFF
+        let blue = color & 0xFF
+                
+        return vector_float4(Float(red)/255, Float(green)/255, Float(blue)/255, (Float(alpha)/255))
+    }
+}

+ 54 - 0
spine-ios/Sources/Spine/Extensions/SkeletonDrawableWrapper+CGImage.swift

@@ -0,0 +1,54 @@
+import Foundation
+import UIKit
+import CoreGraphics
+
+public extension SkeletonDrawableWrapper {
+    
+    /// Render the ``Skeleton`` to a `CGImage`
+    ///
+    /// Parameters:
+    ///     - size: The size of the `CGImage` that should be rendered.
+    ///     - backgroundColor: the background color of the image
+    ///     - scaleFactor: The scale factor. Set this to `UIScreen.main.scale` if you want to show the image in a view
+    func renderToImage(size: CGSize, backgroundColor: UIColor, scaleFactor: CGFloat = 1) throws -> CGImage? {
+        let spineView = SpineUIView(
+            controller: SpineController(disposeDrawableOnDeInit: false), // Doesn't own the drawable
+            backgroundColor: backgroundColor
+        )
+        spineView.frame = CGRect(origin: .zero, size: size)
+        spineView.isPaused = false
+        spineView.enableSetNeedsDisplay = false
+        spineView.framebufferOnly = false
+        spineView.contentScaleFactor = scaleFactor
+        
+        try spineView.load(drawable: self)
+        spineView.renderer?.waitUntilCompleted = true
+        
+        spineView.delegate?.draw(in: spineView)
+        
+        guard let texture = spineView.currentDrawable?.texture else {
+            throw "Could not read texture."
+        }
+        let width = texture.width
+        let height = texture.height
+        let rowBytes = width * 4
+        let data = UnsafeMutableRawPointer.allocate(byteCount: rowBytes * height, alignment: MemoryLayout<UInt8>.alignment)
+        defer {
+            data.deallocate()
+        }
+        
+        let region = MTLRegionMake2D(0, 0, width, height)
+        texture.getBytes(data, bytesPerRow: rowBytes, from: region, mipmapLevel: 0)
+        
+        let bitmapInfo = CGBitmapInfo(
+            rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue
+        ).union(.byteOrder32Little)
+        
+        let colorSpace = CGColorSpaceCreateDeviceRGB()
+        guard let context = CGContext(data: data, width: width, height: height, bitsPerComponent: 8, bytesPerRow: rowBytes, space: colorSpace, bitmapInfo: bitmapInfo.rawValue),
+              let cgImage = context.makeImage() else {
+                throw "Could not create image."
+        }
+        return cgImage
+    }
+}

+ 19 - 0
spine-ios/Sources/Spine/Metal/SpineObjects.swift

@@ -0,0 +1,19 @@
+import Foundation
+import MetalKit
+
+/// Shared objects that live throughout applications lifecycle
+///
+/// Persistent Objects
+/// https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/PersistentObjects.html#//apple_ref/doc/uid/TP40016642-CH3-SW1
+internal final class SpineObjects {
+    
+    static let shared = SpineObjects()
+    
+    internal lazy var device: MTLDevice = {
+        MTLCreateSystemDefaultDevice()!
+    }()
+    
+    internal lazy var commandQueue: MTLCommandQueue = {
+        device.makeCommandQueue()!
+    }()
+}

+ 322 - 0
spine-ios/Sources/Spine/Metal/SpineRenderer.swift

@@ -0,0 +1,322 @@
+import Foundation
+import MetalKit
+import SpineShadersStructs
+import Spine
+import SpineCppLite
+
+protocol SpineRendererDelegate: AnyObject {
+    func spineRendererWillUpdate(_ spineRenderer: SpineRenderer)
+    func spineRenderer(_ spineRenderer: SpineRenderer, needsUpdate delta: TimeInterval)
+    func spineRendererDidUpdate(_ spineRenderer: SpineRenderer)
+    
+    func spineRendererWillDraw(_ spineRenderer: SpineRenderer)
+    func spineRendererDidDraw(_ spineRenderer: SpineRenderer)
+    
+    func spineRendererDidUpdate(_ spineRenderer: SpineRenderer, scaleX: CGFloat, scaleY: CGFloat, offsetX: CGFloat, offsetY: CGFloat, size: CGSize)
+}
+
+protocol SpineRendererDataSource: AnyObject {
+    func isPlaying(_ spineRenderer: SpineRenderer) -> Bool
+    func renderCommands(_ spineRenderer: SpineRenderer) -> [RenderCommand]
+}
+
+internal final class SpineRenderer: NSObject, MTKViewDelegate {
+    
+    private let device: MTLDevice
+    private let textures: [MTLTexture]
+    private let commandQueue: MTLCommandQueue
+    
+    private var sizeInPoints: CGSize = .zero
+    private var viewPortSize = vector_uint2(0, 0)
+    private var transform = SpineTransform(
+        translation: vector_float2(0, 0),
+        scale: vector_float2(1, 1),
+        offset: vector_float2(0, 0)
+    )
+    internal var lastDraw: CFTimeInterval = 0
+    internal var waitUntilCompleted = false
+    private var pipelineStatesByBlendMode = [Int: MTLRenderPipelineState]()
+    
+    private static let numberOfBuffers = 3
+    private static let defaultBufferSize = 32 * 1024 // 32KB
+    
+    private var buffers = [MTLBuffer]()
+    private let bufferingSemaphore = DispatchSemaphore(value: SpineRenderer.numberOfBuffers)
+    private var currentBufferIndex: Int = 0
+    
+    weak var dataSource: SpineRendererDataSource?
+    weak var delegate: SpineRendererDelegate?
+    
+    internal init(
+        device: MTLDevice,
+        commandQueue: MTLCommandQueue,
+        pixelFormat: MTLPixelFormat,
+        atlasPages: [UIImage],
+        pma: Bool
+    ) throws {
+        self.device = device
+        self.commandQueue = commandQueue
+        
+        let bundle: Bundle
+        #if SWIFT_PACKAGE // SPM
+        bundle = .module
+        #else // CocoaPods
+        bundle = Bundle(for: SpineRenderer.self)
+        #endif
+        
+        let defaultLibrary = try device.makeDefaultLibrary(bundle: bundle)
+        let textureLoader = MTKTextureLoader(device: device)
+        textures = try atlasPages
+            .compactMap { $0.cgImage }
+            .map {
+                try textureLoader.newTexture(
+                    cgImage: $0,
+                    options: [
+                        .textureUsage: NSNumber(value: MTLTextureUsage.shaderRead.rawValue),
+                        .SRGB: false,
+                    ]
+                )
+            }
+        
+        let blendModes = [
+            SPINE_BLEND_MODE_NORMAL,
+            SPINE_BLEND_MODE_ADDITIVE,
+            SPINE_BLEND_MODE_MULTIPLY,
+            SPINE_BLEND_MODE_SCREEN
+        ]
+        for blendMode in blendModes {
+            let descriptor = MTLRenderPipelineDescriptor()
+            descriptor.vertexFunction = defaultLibrary.makeFunction(name: "vertexShader")
+            descriptor.fragmentFunction = defaultLibrary.makeFunction(name: "fragmentShader")
+            descriptor.colorAttachments[0].pixelFormat = pixelFormat
+            descriptor.colorAttachments[0].apply(
+                blendMode: blendMode,
+                with: pma
+            )
+            pipelineStatesByBlendMode[Int(blendMode.rawValue)] = try device.makeRenderPipelineState(descriptor: descriptor)
+        }
+        
+        super.init()
+                
+        increaseBuffersSize(to: SpineRenderer.defaultBufferSize)
+    }
+    
+    func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
+        guard let spineView = view as? SpineUIView else { return }
+        
+        sizeInPoints = CGSize(width: size.width / UIScreen.main.scale, height: size.height / UIScreen.main.scale)
+        viewPortSize = vector_uint2(UInt32(size.width), UInt32(size.height))
+        setTransform(
+            bounds: spineView.computedBounds,
+            mode: spineView.mode,
+            alignment: spineView.alignment
+        )
+    }
+    
+    func draw(in view: MTKView) {
+        guard dataSource?.isPlaying(self) ?? false else {
+            lastDraw = CACurrentMediaTime()
+            return
+        }
+        
+        callNeedsUpdate()
+        
+        // Tripple Buffering
+        // Source: https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/TripleBuffering.html#//apple_ref/doc/uid/TP40016642-CH5-SW1
+        bufferingSemaphore.wait()
+        currentBufferIndex = (currentBufferIndex + 1) % SpineRenderer.numberOfBuffers
+        
+        guard let renderCommands = dataSource?.renderCommands(self),
+              let commandBuffer = commandQueue.makeCommandBuffer(),
+              let renderPassDescriptor = view.currentRenderPassDescriptor,
+              let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
+            return
+        }
+        
+        delegate?.spineRendererWillDraw(self)
+        draw(renderCommands: renderCommands, renderEncoder: renderEncoder, in: view)
+        delegate?.spineRendererDidDraw(self)
+        
+        renderEncoder.endEncoding()
+        view.currentDrawable.flatMap {
+            commandBuffer.present($0)
+        }
+        commandBuffer.addCompletedHandler { [bufferingSemaphore] _ in
+            bufferingSemaphore.signal()
+        }
+        commandBuffer.commit()
+        if waitUntilCompleted {
+            commandBuffer.waitUntilCompleted()
+        }
+    }
+    
+    private func setTransform(bounds: CGRect, mode: Spine.ContentMode, alignment: Spine.Alignment) {
+        let x = -bounds.minX - bounds.width / 2.0
+        let y = -bounds.minY - bounds.height / 2.0
+        
+        var scaleX: CGFloat = 1.0
+        var scaleY: CGFloat = 1.0
+        
+        switch mode {
+        case .fit:
+            scaleX = min(sizeInPoints.width / bounds.width, sizeInPoints.height / bounds.height)
+            scaleY = scaleX
+        case .fill:
+            scaleX = max(sizeInPoints.width / bounds.width, sizeInPoints.height / bounds.height)
+            scaleY = scaleX
+        }
+        
+        let offsetX = abs(sizeInPoints.width - bounds.width * scaleX) / 2 * alignment.x
+        let offsetY = abs(sizeInPoints.height - bounds.height * scaleY) / 2 * alignment.y
+        
+        transform = SpineTransform(
+            translation: vector_float2(Float(x), Float(y)),
+            scale: vector_float2(Float(scaleX * UIScreen.main.scale), Float(scaleY * UIScreen.main.scale)),
+            offset: vector_float2(Float(offsetX * UIScreen.main.scale), Float(offsetY * UIScreen.main.scale))
+        )
+        
+        delegate?.spineRendererDidUpdate(
+            self,
+            scaleX: scaleX,
+            scaleY: scaleY,
+            offsetX: x + offsetX / scaleX,
+            offsetY: y + offsetY / scaleY,
+            size: sizeInPoints
+        )
+    }
+    
+    private func callNeedsUpdate() {
+        if lastDraw == 0 {
+            lastDraw = CACurrentMediaTime()
+        }
+        let delta = CACurrentMediaTime() - lastDraw
+        delegate?.spineRendererWillUpdate(self)
+        delegate?.spineRenderer(self, needsUpdate: delta)
+        lastDraw = CACurrentMediaTime()
+        delegate?.spineRendererDidUpdate(self)
+    }
+        
+    private func draw(renderCommands: [RenderCommand], renderEncoder: MTLRenderCommandEncoder, in view: MTKView) {
+        let allVertices = renderCommands.map { renderCommand in
+            Array(renderCommand.getVertices())
+        }
+        let vertices = allVertices.flatMap { $0 }
+        let verticesSize = MemoryLayout<SpineVertex>.stride * vertices.count
+        
+        guard verticesSize > 0 else {
+            return
+        }
+        
+        var vertexBuffer = buffers[currentBufferIndex]
+        var vertexBufferSize = vertexBuffer.length
+        
+        if vertexBufferSize < verticesSize {
+            increaseBuffersSize(to: verticesSize)
+            vertexBuffer = buffers[currentBufferIndex]
+        }
+        
+        renderEncoder.setViewport(
+            MTLViewport(
+                originX: 0.0,
+                originY: 0.0,
+                width: Double(viewPortSize.x),
+                height: Double(viewPortSize.y),
+                znear: 0.0,
+                zfar: 1.0
+            )
+        )
+        
+        memcpy(vertexBuffer.contents(), vertices, verticesSize)
+        
+        renderEncoder.setVertexBuffer(
+            vertexBuffer,
+            offset: 0,
+            index: Int(SpineVertexInputIndexVertices.rawValue)
+        )
+        renderEncoder.setVertexBytes(
+            &transform,
+            length: MemoryLayout.size(ofValue: transform),
+            index: Int(SpineVertexInputIndexTransform.rawValue)
+        )
+        renderEncoder.setVertexBytes(
+            &viewPortSize,
+            length: MemoryLayout.size(ofValue: viewPortSize),
+            index: Int(SpineVertexInputIndexViewportSize.rawValue)
+        )
+        
+        // Buffer Bindings
+        // https://developer.apple.com/library/archive/documentation/3DDrawing/Conceptual/MTLBestPracticesGuide/BufferBindings.html#//apple_ref/doc/uid/TP40016642-CH28-SW3
+        var vertexStart = 0
+        for (index, renderCommand) in renderCommands.enumerated() {
+            guard let pipelineState = getPipelineState(blendMode: renderCommand.blendMode) else {
+                continue
+            }
+            renderEncoder.setRenderPipelineState(pipelineState)
+            
+            let vertices = allVertices[index]
+            
+            let textureIndex = Int(renderCommand.atlasPage)
+            if textures.indices.contains(textureIndex) {
+                renderEncoder.setFragmentTexture(
+                    textures[textureIndex],
+                    index: Int(SpineTextureIndexBaseColor.rawValue)
+                )
+            }
+            
+            renderEncoder.drawPrimitives(
+                type: .triangle,
+                vertexStart: vertexStart,
+                vertexCount: vertices.count
+            )
+            vertexStart += vertices.count
+        }
+    }
+    
+    private func getPipelineState(blendMode: BlendMode) -> MTLRenderPipelineState? {
+        pipelineStatesByBlendMode[Int(blendMode.rawValue)]
+    }
+    
+    private func increaseBuffersSize(to size: Int) {
+        buffers = (0 ..< SpineRenderer.numberOfBuffers).map { _ in
+            device.makeBuffer(length: size, options: .storageModeShared)!
+        }
+    }
+}
+
+fileprivate extension MTLRenderPipelineColorAttachmentDescriptor {
+    
+    func apply(blendMode: BlendMode, with premultipliedAlpha: Bool) {
+        isBlendingEnabled = true
+        sourceRGBBlendFactor = blendMode.sourceRGBBlendFactor(premultipliedAlpha: premultipliedAlpha)
+        destinationRGBBlendFactor = blendMode.destinationRGBBlendFactor
+        destinationAlphaBlendFactor = .oneMinusSourceAlpha
+    }
+}
+
+fileprivate extension BlendMode {
+    func sourceRGBBlendFactor(premultipliedAlpha: Bool) -> MTLBlendFactor {
+        switch self {
+        case SPINE_BLEND_MODE_NORMAL, SPINE_BLEND_MODE_ADDITIVE:
+            return premultipliedAlpha ? .one : .sourceAlpha
+        case SPINE_BLEND_MODE_MULTIPLY:
+            return .destinationColor
+        case SPINE_BLEND_MODE_SCREEN:
+            return .one
+        default:
+            return .one // Should never be called
+        }
+    }
+    
+    var destinationRGBBlendFactor: MTLBlendFactor {
+        switch self {
+        case SPINE_BLEND_MODE_NORMAL, SPINE_BLEND_MODE_ADDITIVE:
+            return .oneMinusSourceAlpha
+        case SPINE_BLEND_MODE_MULTIPLY:
+            return .one
+        case SPINE_BLEND_MODE_SCREEN:
+            return .oneMinusSourceColor
+        default:
+            return .one // Should never be called
+        }
+    }
+}

+ 70 - 0
spine-ios/Sources/Spine/Metal/SpineShaders.metal

@@ -0,0 +1,70 @@
+#include <metal_stdlib>
+
+using namespace metal;
+
+typedef enum SpineVertexInputIndex {
+    SpineVertexInputIndexVertices     = 0,
+    SpineVertexInputIndexTransform    = 1,
+    SpineVertexInputIndexViewportSize = 2,
+} SpineVertexInputIndex;
+
+typedef enum SpineTextureIndex {
+    SpineTextureIndexBaseColor = 0,
+} SpineTextureIndex;
+
+typedef struct {
+    vector_float2 position;
+    vector_float4 color;
+    vector_float2 uv;
+} SpineVertex;
+
+typedef struct {
+    vector_float2 translation;
+    vector_float2 scale;
+    vector_float2 offset;
+} SpineTransform;
+
+struct RasterizerData {
+    float4 position [[position]];
+    float4 color;
+    float2 textureCoordinate;
+};
+
+vertex RasterizerData
+vertexShader(uint vertexID [[vertex_id]],
+             constant SpineVertex *vertices [[buffer(SpineVertexInputIndexVertices)]],
+             constant SpineTransform *transform [[buffer(SpineVertexInputIndexTransform)]],
+             constant vector_uint2 *viewportSizePointer [[buffer(SpineVertexInputIndexViewportSize)]])
+{
+    RasterizerData out;
+
+    float2 pixelSpacePosition = vertices[vertexID].position.xy;
+
+    vector_float2 viewportSize = vector_float2(*viewportSizePointer);
+    
+    out.position = vector_float4(0.0, 0.0, 0.0, 1.0);
+    
+    out.position.xy = pixelSpacePosition;
+    out.position.xy *= transform->scale;
+    out.position.xy += transform->translation * transform->scale + transform->offset;
+    out.position.xy /= viewportSize / 2;
+    out.position.y *= -1;
+    
+    out.color = vertices[vertexID].color;
+    
+    out.textureCoordinate = vertices[vertexID].uv;
+    
+    return out;
+}
+
+fragment float4
+fragmentShader(RasterizerData in [[stage_in]],
+               texture2d<half> colorTexture [[ texture(SpineTextureIndexBaseColor) ]])
+{
+    constexpr sampler textureSampler (mag_filter::nearest,
+                                      min_filter::nearest);
+    
+    const half4 colorSample = colorTexture.sample(textureSampler, in.textureCoordinate);
+    
+    return float4(colorSample) * in.color;
+}

+ 145 - 0
spine-ios/Sources/Spine/SkeletonDrawableWrapper.swift

@@ -0,0 +1,145 @@
+import Foundation
+import Spine
+import SpineCppLite
+import CoreGraphics
+import UIKit
+
+/// A ``SkeletonDrawableWrapper`` with ``SkeletonDrawable`` bundle loading, updating, and rendering an ``Atlas``, ``Skeleton``, and ``AnimationState``
+/// into a single easy to use class.
+///
+/// Use the ``SkeletonDrawableWrapper/fromBundle(atlasFileName:skeletonFileName:bundle:)``, ``SkeletonDrawableWrapper/fromFile(atlasFile:skeletonFile:)``, or ``SkeletonDrawableWrapper/fromHttp(atlasURL:skeletonURL:)`` methods to construct a ``SkeletonDrawableWrapper``. To have
+/// multiple skeleton drawable wrapper instances share the same ``Atlas`` and ``SkeletonData``, use the constructor.
+///
+/// You can then directly access the `skeletonDrawable` and with it `atlas`, `skeletonData`, `skeleton`, `animationStateData`, `animationState` and `animationStateWrapper`.
+/// to query and animate the skeleton. Use the ``AnimationStateWrapper`` to queue animations on one or more tracks
+/// via ``AnimationState/setAnimation(trackIndex:animation:loop:)`` or ``AnimationState/addAnimation(trackIndex:animation:loop:delay:)``.
+///
+/// To update the ``AnimationState`` and apply it to the ``Skeleton`` call the ``AnimationStateWrapper/update`` function, providing it
+/// a delta time in seconds to advance the animations.
+///
+/// To render the current pose of the ``Skeleton`` as a `CGImage`, use ``SkeletonDrawableWrapper/renderToImage(size:backgroundColor:scaleFactor:)``.
+///
+/// When the skeleton drawable is no longer needed, call the ``SkeletonDrawableWrapper/dispose()`` method to release its resources. If
+/// the skeleton drawable was constructed from a shared ``Atlas`` and ``SkeletonData``, make sure to dispose the
+/// atlas and skeleton data as well, if no skeleton drawable references them anymore.
+@objc(SpineSkeletonDrawableWrapper)
+@objcMembers
+public final class SkeletonDrawableWrapper: NSObject {
+    
+    public let atlas: Atlas
+    public let atlasPages: [UIImage]
+    public let skeletonData: SkeletonData
+    
+    public let skeletonDrawable: SkeletonDrawable
+    public let skeleton: Skeleton
+    public let animationStateData: AnimationStateData
+    public let animationState: AnimationState
+    public let animationStateWrapper: AnimationStateWrapper
+    
+    internal var disposed = false
+    
+    /// Constructs a new skeleton drawable from the `atlasFileName` and `skeletonFileName` from the `main` bundle
+    /// or the optionally provided `bundle`.
+    ///
+    /// Throws an `Error` in case the data could not be loaded.
+    public static func fromBundle(atlasFileName: String, skeletonFileName: String, bundle: Bundle = .main) async throws -> SkeletonDrawableWrapper {
+        let atlasAndPages = try await Atlas.fromBundle(atlasFileName, bundle: bundle)
+        let skeletonData = try await SkeletonData.fromBundle(
+            atlas: atlasAndPages.0,
+            skeletonFileName: skeletonFileName,
+            bundle: bundle
+        )
+        return try SkeletonDrawableWrapper(
+            atlas: atlasAndPages.0,
+            atlasPages: atlasAndPages.1,
+            skeletonData: skeletonData
+        )
+    }
+    
+    /// Constructs a new skeleton drawable from the `atlasFile` and `skeletonFile`.
+    ///
+    /// Throws an `Error` in case the data could not be loaded.
+    public static func fromFile(atlasFile: URL, skeletonFile: URL) async throws -> SkeletonDrawableWrapper {
+        let atlasAndPages = try await Atlas.fromFile(atlasFile)
+        let skeletonData = try await SkeletonData.fromFile(
+            atlas: atlasAndPages.0,
+            skeletonFile: skeletonFile
+        )
+        return try SkeletonDrawableWrapper(
+            atlas: atlasAndPages.0,
+            atlasPages: atlasAndPages.1,
+            skeletonData: skeletonData
+        )
+    }
+    
+    /// Constructs a new skeleton drawable wrapper from the http `atlasUrl` and `skeletonUrl`.
+    ///
+    /// Throws an `Error` in case the data could not be loaded.
+    public static func fromHttp(atlasURL: URL, skeletonURL: URL) async throws -> SkeletonDrawableWrapper {
+        let atlasAndPages = try await Atlas.fromHttp(atlasURL)
+        let skeletonData = try await SkeletonData.fromHttp(
+            atlas: atlasAndPages.0,
+            skeletonURL: skeletonURL
+        )
+        return try SkeletonDrawableWrapper(
+            atlas: atlasAndPages.0,
+            atlasPages: atlasAndPages.1,
+            skeletonData: skeletonData
+        )
+    }
+    
+    public init(atlas: Atlas, atlasPages: [UIImage], skeletonData: SkeletonData) throws {
+        self.atlas = atlas
+        self.atlasPages = atlasPages
+        self.skeletonData = skeletonData
+            
+        guard let nativeSkeletonDrawable = spine_skeleton_drawable_create(skeletonData.wrappee) else {
+            throw "Could not load native skeleton drawable"
+        }
+        skeletonDrawable = SkeletonDrawable(nativeSkeletonDrawable)
+        
+        guard let nativeSkeleton = spine_skeleton_drawable_get_skeleton(skeletonDrawable.wrappee) else {
+            throw "Could not load native skeleton"
+        }
+        skeleton = Skeleton(nativeSkeleton)
+        
+        guard let nativeAnimationStateData = spine_skeleton_drawable_get_animation_state_data(skeletonDrawable.wrappee) else {
+            throw "Could not load native animation state data"
+        }
+        animationStateData = AnimationStateData(nativeAnimationStateData)
+        
+        guard let nativeAnimationState = spine_skeleton_drawable_get_animation_state(skeletonDrawable.wrappee) else {
+            throw "Could not load native animation state"
+        }
+        animationState = AnimationState(nativeAnimationState)
+        animationStateWrapper = AnimationStateWrapper(
+            animationState: animationState,
+            aninationStateEvents: skeletonDrawable.animationStateEvents
+        )
+        skeleton.updateWorldTransform(physics: SPINE_PHYSICS_NONE)
+        super.init()
+    }
+    
+    /// Updates the ``AnimationState`` using the `delta` time given in seconds, applies the
+    /// animation state to the ``Skeleton`` and updates the world transforms of the skeleton
+    /// to calculate its current pose.
+    public func update(delta: Float) {
+        if disposed { return }
+        
+        animationStateWrapper.update(delta: delta)
+        animationState.apply(skeleton: skeleton)
+        
+        skeleton.update(delta: delta)
+        skeleton.updateWorldTransform(physics: SPINE_PHYSICS_UPDATE)
+    }
+    
+    public func dispose() {
+        if disposed { return }
+        disposed = true
+        
+        atlas.dispose()
+        skeletonData.dispose()
+        
+        skeletonDrawable.dispose()
+    }
+}

+ 315 - 0
spine-ios/Sources/Spine/Spine.Generated+Extensions.swift

@@ -0,0 +1,315 @@
+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 {
+    
+}

+ 3721 - 0
spine-ios/Sources/Spine/Spine.Generated.swift

@@ -0,0 +1,3721 @@
+import Foundation
+import SpineCppLite
+
+public typealias BlendMode = spine_blend_mode
+public typealias MixBlend = spine_mix_blend
+public typealias EventType = spine_event_type
+public typealias AttachmentType = spine_attachment_type
+public typealias ConstraintType = spine_constraint_type
+public typealias Inherit = spine_inherit
+public typealias PositionMode = spine_position_mode
+public typealias SpacingMode = spine_spacing_mode
+public typealias RotateMode = spine_rotate_mode
+public typealias Physics = spine_physics
+
+@objc(SpineTransformConstraintData)
+@objcMembers
+public final class TransformConstraintData: NSObject {
+
+    internal let wrappee: spine_transform_constraint_data
+
+    internal init(_ wrappee: spine_transform_constraint_data) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var bones: [BoneData] {
+        let num = Int(spine_transform_constraint_data_get_num_bones(wrappee))
+        let ptr = spine_transform_constraint_data_get_bones(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var target: BoneData {
+        get {
+            return .init(spine_transform_constraint_data_get_target(wrappee))
+        }
+        set {
+            spine_transform_constraint_data_set_target(wrappee, newValue.wrappee)
+        }
+    }
+
+    public var mixRotate: Float {
+        get {
+            return spine_transform_constraint_data_get_mix_rotate(wrappee)
+        }
+        set {
+            spine_transform_constraint_data_set_mix_rotate(wrappee, newValue)
+        }
+    }
+
+    public var mixX: Float {
+        get {
+            return spine_transform_constraint_data_get_mix_x(wrappee)
+        }
+        set {
+            spine_transform_constraint_data_set_mix_x(wrappee, newValue)
+        }
+    }
+
+    public var mixY: Float {
+        get {
+            return spine_transform_constraint_data_get_mix_y(wrappee)
+        }
+        set {
+            spine_transform_constraint_data_set_mix_y(wrappee, newValue)
+        }
+    }
+
+    public var mixScaleX: Float {
+        get {
+            return spine_transform_constraint_data_get_mix_scale_x(wrappee)
+        }
+        set {
+            spine_transform_constraint_data_set_mix_scale_x(wrappee, newValue)
+        }
+    }
+
+    public var mixScaleY: Float {
+        get {
+            return spine_transform_constraint_data_get_mix_scale_y(wrappee)
+        }
+        set {
+            spine_transform_constraint_data_set_mix_scale_y(wrappee, newValue)
+        }
+    }
+
+    public var mixShearY: Float {
+        get {
+            return spine_transform_constraint_data_get_mix_shear_y(wrappee)
+        }
+        set {
+            spine_transform_constraint_data_set_mix_shear_y(wrappee, newValue)
+        }
+    }
+
+    public var offsetRotation: Float {
+        get {
+            return spine_transform_constraint_data_get_offset_rotation(wrappee)
+        }
+        set {
+            spine_transform_constraint_data_set_offset_rotation(wrappee, newValue)
+        }
+    }
+
+    public var offsetX: Float {
+        get {
+            return spine_transform_constraint_data_get_offset_x(wrappee)
+        }
+        set {
+            spine_transform_constraint_data_set_offset_x(wrappee, newValue)
+        }
+    }
+
+    public var offsetY: Float {
+        get {
+            return spine_transform_constraint_data_get_offset_y(wrappee)
+        }
+        set {
+            spine_transform_constraint_data_set_offset_y(wrappee, newValue)
+        }
+    }
+
+    public var offsetScaleX: Float {
+        get {
+            return spine_transform_constraint_data_get_offset_scale_x(wrappee)
+        }
+        set {
+            spine_transform_constraint_data_set_offset_scale_x(wrappee, newValue)
+        }
+    }
+
+    public var offsetScaleY: Float {
+        get {
+            return spine_transform_constraint_data_get_offset_scale_y(wrappee)
+        }
+        set {
+            spine_transform_constraint_data_set_offset_scale_y(wrappee, newValue)
+        }
+    }
+
+    public var offsetShearY: Float {
+        get {
+            return spine_transform_constraint_data_get_offset_shear_y(wrappee)
+        }
+        set {
+            spine_transform_constraint_data_set_offset_shear_y(wrappee, newValue)
+        }
+    }
+
+    public var isRelative: Bool {
+        get {
+            return spine_transform_constraint_data_get_is_relative(wrappee) != 0
+        }
+        set {
+            spine_transform_constraint_data_set_is_relative(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var isLocal: Bool {
+        get {
+            return spine_transform_constraint_data_get_is_local(wrappee) != 0
+        }
+        set {
+            spine_transform_constraint_data_set_is_local(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+}
+
+@objc(SpineBoundingBoxAttachment)
+@objcMembers
+public final class BoundingBoxAttachment: NSObject {
+
+    internal let wrappee: spine_bounding_box_attachment
+
+    internal init(_ wrappee: spine_bounding_box_attachment) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var color: Color {
+        return .init(spine_bounding_box_attachment_get_color(wrappee))
+    }
+
+    public func setColor(r: Float, g: Float, b: Float, a: Float) {
+        spine_bounding_box_attachment_set_color(wrappee, r, g, b, a)
+    }
+
+}
+
+@objc(SpinePhysicsConstraintData)
+@objcMembers
+public final class PhysicsConstraintData: NSObject {
+
+    internal let wrappee: spine_physics_constraint_data
+
+    internal init(_ wrappee: spine_physics_constraint_data) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var bone: BoneData {
+        get {
+            return .init(spine_physics_constraint_data_get_bone(wrappee))
+        }
+        set {
+            spine_physics_constraint_data_set_bone(wrappee, newValue.wrappee)
+        }
+    }
+
+    public var x: Float {
+        get {
+            return spine_physics_constraint_data_get_x(wrappee)
+        }
+        set {
+            spine_physics_constraint_data_set_x(wrappee, newValue)
+        }
+    }
+
+    public var y: Float {
+        get {
+            return spine_physics_constraint_data_get_y(wrappee)
+        }
+        set {
+            spine_physics_constraint_data_set_y(wrappee, newValue)
+        }
+    }
+
+    public var rotate: Float {
+        get {
+            return spine_physics_constraint_data_get_rotate(wrappee)
+        }
+        set {
+            spine_physics_constraint_data_set_rotate(wrappee, newValue)
+        }
+    }
+
+    public var scaleX: Float {
+        get {
+            return spine_physics_constraint_data_get_scale_x(wrappee)
+        }
+        set {
+            spine_physics_constraint_data_set_scale_x(wrappee, newValue)
+        }
+    }
+
+    public var shearX: Float {
+        get {
+            return spine_physics_constraint_data_get_shear_x(wrappee)
+        }
+        set {
+            spine_physics_constraint_data_set_shear_x(wrappee, newValue)
+        }
+    }
+
+    public var limit: Float {
+        get {
+            return spine_physics_constraint_data_get_limit(wrappee)
+        }
+        set {
+            spine_physics_constraint_data_set_limit(wrappee, newValue)
+        }
+    }
+
+    public var step: Float {
+        get {
+            return spine_physics_constraint_data_get_step(wrappee)
+        }
+        set {
+            spine_physics_constraint_data_set_step(wrappee, newValue)
+        }
+    }
+
+    public var inertia: Float {
+        get {
+            return spine_physics_constraint_data_get_inertia(wrappee)
+        }
+        set {
+            spine_physics_constraint_data_set_inertia(wrappee, newValue)
+        }
+    }
+
+    public var strength: Float {
+        get {
+            return spine_physics_constraint_data_get_strength(wrappee)
+        }
+        set {
+            spine_physics_constraint_data_set_strength(wrappee, newValue)
+        }
+    }
+
+    public var damping: Float {
+        get {
+            return spine_physics_constraint_data_get_damping(wrappee)
+        }
+        set {
+            spine_physics_constraint_data_set_damping(wrappee, newValue)
+        }
+    }
+
+    public var massInverse: Float {
+        get {
+            return spine_physics_constraint_data_get_mass_inverse(wrappee)
+        }
+        set {
+            spine_physics_constraint_data_set_mass_inverse(wrappee, newValue)
+        }
+    }
+
+    public var wind: Float {
+        get {
+            return spine_physics_constraint_data_get_wind(wrappee)
+        }
+        set {
+            spine_physics_constraint_data_set_wind(wrappee, newValue)
+        }
+    }
+
+    public var gravity: Float {
+        get {
+            return spine_physics_constraint_data_get_gravity(wrappee)
+        }
+        set {
+            spine_physics_constraint_data_set_gravity(wrappee, newValue)
+        }
+    }
+
+    public var mix: Float {
+        get {
+            return spine_physics_constraint_data_get_mix(wrappee)
+        }
+        set {
+            spine_physics_constraint_data_set_mix(wrappee, newValue)
+        }
+    }
+
+    public var isInertiaGlobal: Bool {
+        get {
+            return spine_physics_constraint_data_is_inertia_global(wrappee) != 0
+        }
+        set {
+            spine_physics_constraint_data_set_inertia_global(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var isStrengthGlobal: Bool {
+        get {
+            return spine_physics_constraint_data_is_strength_global(wrappee) != 0
+        }
+        set {
+            spine_physics_constraint_data_set_strength_global(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var isDampingGlobal: Bool {
+        get {
+            return spine_physics_constraint_data_is_damping_global(wrappee) != 0
+        }
+        set {
+            spine_physics_constraint_data_set_damping_global(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var isMassGlobal: Bool {
+        get {
+            return spine_physics_constraint_data_is_mass_global(wrappee) != 0
+        }
+        set {
+            spine_physics_constraint_data_set_mass_global(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var isWindGlobal: Bool {
+        get {
+            return spine_physics_constraint_data_is_wind_global(wrappee) != 0
+        }
+        set {
+            spine_physics_constraint_data_set_wind_global(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var isGravityGlobal: Bool {
+        get {
+            return spine_physics_constraint_data_is_gravity_global(wrappee) != 0
+        }
+        set {
+            spine_physics_constraint_data_set_gravity_global(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var isMixGlobal: Bool {
+        get {
+            return spine_physics_constraint_data_is_mix_global(wrappee) != 0
+        }
+        set {
+            spine_physics_constraint_data_set_mix_global(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+}
+
+@objc(SpineAnimationStateEvents)
+@objcMembers
+public final class AnimationStateEvents: NSObject {
+
+    internal let wrappee: spine_animation_state_events
+
+    internal init(_ wrappee: spine_animation_state_events) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    @discardableResult
+    public func getEventType(index: Int32) -> EventType {
+        return spine_animation_state_events_get_event_type(wrappee, index)
+    }
+
+    @discardableResult
+    public func getTrackEntry(index: Int32) -> TrackEntry {
+        return .init(spine_animation_state_events_get_track_entry(wrappee, index))
+    }
+
+    @discardableResult
+    public func getEvent(index: Int32) -> Event? {
+        return spine_animation_state_events_get_event(wrappee, index).flatMap { .init($0) }
+    }
+
+    public func reset() {
+        spine_animation_state_events_reset(wrappee)
+    }
+
+}
+
+@objc(SpineTransformConstraint)
+@objcMembers
+public final class TransformConstraint: NSObject {
+
+    internal let wrappee: spine_transform_constraint
+
+    internal init(_ wrappee: spine_transform_constraint) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var order: Int32 {
+        return spine_transform_constraint_get_order(wrappee)
+    }
+
+    public var data: TransformConstraintData {
+        return .init(spine_transform_constraint_get_data(wrappee))
+    }
+
+    public var bones: [Bone] {
+        let num = Int(spine_transform_constraint_get_num_bones(wrappee))
+        let ptr = spine_transform_constraint_get_bones(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var target: Bone {
+        get {
+            return .init(spine_transform_constraint_get_target(wrappee))
+        }
+        set {
+            spine_transform_constraint_set_target(wrappee, newValue.wrappee)
+        }
+    }
+
+    public var mixRotate: Float {
+        get {
+            return spine_transform_constraint_get_mix_rotate(wrappee)
+        }
+        set {
+            spine_transform_constraint_set_mix_rotate(wrappee, newValue)
+        }
+    }
+
+    public var mixX: Float {
+        get {
+            return spine_transform_constraint_get_mix_x(wrappee)
+        }
+        set {
+            spine_transform_constraint_set_mix_x(wrappee, newValue)
+        }
+    }
+
+    public var mixY: Float {
+        get {
+            return spine_transform_constraint_get_mix_y(wrappee)
+        }
+        set {
+            spine_transform_constraint_set_mix_y(wrappee, newValue)
+        }
+    }
+
+    public var mixScaleX: Float {
+        get {
+            return spine_transform_constraint_get_mix_scale_x(wrappee)
+        }
+        set {
+            spine_transform_constraint_set_mix_scale_x(wrappee, newValue)
+        }
+    }
+
+    public var mixScaleY: Float {
+        get {
+            return spine_transform_constraint_get_mix_scale_y(wrappee)
+        }
+        set {
+            spine_transform_constraint_set_mix_scale_y(wrappee, newValue)
+        }
+    }
+
+    public var mixShearY: Float {
+        get {
+            return spine_transform_constraint_get_mix_shear_y(wrappee)
+        }
+        set {
+            spine_transform_constraint_set_mix_shear_y(wrappee, newValue)
+        }
+    }
+
+    public var isActive: Bool {
+        get {
+            return spine_transform_constraint_get_is_active(wrappee) != 0
+        }
+        set {
+            spine_transform_constraint_set_is_active(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public func update() {
+        spine_transform_constraint_update(wrappee)
+    }
+
+}
+
+@objc(SpinePathConstraintData)
+@objcMembers
+public final class PathConstraintData: NSObject {
+
+    internal let wrappee: spine_path_constraint_data
+
+    internal init(_ wrappee: spine_path_constraint_data) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var bones: [BoneData] {
+        let num = Int(spine_path_constraint_data_get_num_bones(wrappee))
+        let ptr = spine_path_constraint_data_get_bones(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var target: SlotData {
+        get {
+            return .init(spine_path_constraint_data_get_target(wrappee))
+        }
+        set {
+            spine_path_constraint_data_set_target(wrappee, newValue.wrappee)
+        }
+    }
+
+    public var positionMode: PositionMode {
+        get {
+            return spine_path_constraint_data_get_position_mode(wrappee)
+        }
+        set {
+            spine_path_constraint_data_set_position_mode(wrappee, newValue)
+        }
+    }
+
+    public var spacingMode: SpacingMode {
+        get {
+            return spine_path_constraint_data_get_spacing_mode(wrappee)
+        }
+        set {
+            spine_path_constraint_data_set_spacing_mode(wrappee, newValue)
+        }
+    }
+
+    public var rotateMode: RotateMode {
+        get {
+            return spine_path_constraint_data_get_rotate_mode(wrappee)
+        }
+        set {
+            spine_path_constraint_data_set_rotate_mode(wrappee, newValue)
+        }
+    }
+
+    public var offsetRotation: Float {
+        get {
+            return spine_path_constraint_data_get_offset_rotation(wrappee)
+        }
+        set {
+            spine_path_constraint_data_set_offset_rotation(wrappee, newValue)
+        }
+    }
+
+    public var position: Float {
+        get {
+            return spine_path_constraint_data_get_position(wrappee)
+        }
+        set {
+            spine_path_constraint_data_set_position(wrappee, newValue)
+        }
+    }
+
+    public var spacing: Float {
+        get {
+            return spine_path_constraint_data_get_spacing(wrappee)
+        }
+        set {
+            spine_path_constraint_data_set_spacing(wrappee, newValue)
+        }
+    }
+
+    public var mixRotate: Float {
+        get {
+            return spine_path_constraint_data_get_mix_rotate(wrappee)
+        }
+        set {
+            spine_path_constraint_data_set_mix_rotate(wrappee, newValue)
+        }
+    }
+
+    public var mixX: Float {
+        get {
+            return spine_path_constraint_data_get_mix_x(wrappee)
+        }
+        set {
+            spine_path_constraint_data_set_mix_x(wrappee, newValue)
+        }
+    }
+
+    public var mixY: Float {
+        get {
+            return spine_path_constraint_data_get_mix_y(wrappee)
+        }
+        set {
+            spine_path_constraint_data_set_mix_y(wrappee, newValue)
+        }
+    }
+
+}
+
+@objc(SpineAnimationStateData)
+@objcMembers
+public final class AnimationStateData: NSObject {
+
+    internal let wrappee: spine_animation_state_data
+
+    internal init(_ wrappee: spine_animation_state_data) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var skeletonData: SkeletonData {
+        return .init(spine_animation_state_data_get_skeleton_data(wrappee))
+    }
+
+    public var defaultMix: Float {
+        get {
+            return spine_animation_state_data_get_default_mix(wrappee)
+        }
+        set {
+            spine_animation_state_data_set_default_mix(wrappee, newValue)
+        }
+    }
+
+    public func setMix(from: Animation, to: Animation, duration: Float) {
+        spine_animation_state_data_set_mix(wrappee, from.wrappee, to.wrappee, duration)
+    }
+
+    @discardableResult
+    public func getMix(from: Animation, to: Animation) -> Float {
+        return spine_animation_state_data_get_mix(wrappee, from.wrappee, to.wrappee)
+    }
+
+    public func setMixByName(fromName: String?, toName: String?, duration: Float) {
+        spine_animation_state_data_set_mix_by_name(wrappee, fromName, toName, duration)
+    }
+
+    @discardableResult
+    public func getMixByName(fromName: String?, toName: String?) -> Float {
+        return spine_animation_state_data_get_mix_by_name(wrappee, fromName, toName)
+    }
+
+    public func clear() {
+        spine_animation_state_data_clear(wrappee)
+    }
+
+}
+
+@objc(SpineSkeletonDataResult)
+@objcMembers
+public final class SkeletonDataResult: NSObject {
+
+    internal let wrappee: spine_skeleton_data_result
+    internal var disposed = false
+
+    internal init(_ wrappee: spine_skeleton_data_result) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var error: String? {
+        return spine_skeleton_data_result_get_error(wrappee).flatMap { String(cString: $0) }
+    }
+
+    public var data: SkeletonData {
+        return .init(spine_skeleton_data_result_get_data(wrappee))
+    }
+
+    public func dispose() {
+        if disposed { return }
+        disposed = true
+        spine_skeleton_data_result_dispose(wrappee)
+    }
+
+}
+
+@objc(SpineClippingAttachment)
+@objcMembers
+public final class ClippingAttachment: NSObject {
+
+    internal let wrappee: spine_clipping_attachment
+
+    internal init(_ wrappee: spine_clipping_attachment) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var color: Color {
+        return .init(spine_clipping_attachment_get_color(wrappee))
+    }
+
+    public var endSlot: SlotData? {
+        get {
+            return spine_clipping_attachment_get_end_slot(wrappee).flatMap { .init($0) }
+        }
+        set {
+            spine_clipping_attachment_set_end_slot(wrappee, newValue?.wrappee)
+        }
+    }
+
+    public func setColor(r: Float, g: Float, b: Float, a: Float) {
+        spine_clipping_attachment_set_color(wrappee, r, g, b, a)
+    }
+
+}
+
+@objc(SpineIkConstraintData)
+@objcMembers
+public final class IkConstraintData: NSObject {
+
+    internal let wrappee: spine_ik_constraint_data
+
+    internal init(_ wrappee: spine_ik_constraint_data) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var bones: [BoneData] {
+        let num = Int(spine_ik_constraint_data_get_num_bones(wrappee))
+        let ptr = spine_ik_constraint_data_get_bones(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var target: BoneData {
+        get {
+            return .init(spine_ik_constraint_data_get_target(wrappee))
+        }
+        set {
+            spine_ik_constraint_data_set_target(wrappee, newValue.wrappee)
+        }
+    }
+
+    public var bendDirection: Int32 {
+        get {
+            return spine_ik_constraint_data_get_bend_direction(wrappee)
+        }
+        set {
+            spine_ik_constraint_data_set_bend_direction(wrappee, newValue)
+        }
+    }
+
+    public var compress: Bool {
+        get {
+            return spine_ik_constraint_data_get_compress(wrappee) != 0
+        }
+        set {
+            spine_ik_constraint_data_set_compress(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var stretch: Bool {
+        get {
+            return spine_ik_constraint_data_get_stretch(wrappee) != 0
+        }
+        set {
+            spine_ik_constraint_data_set_stretch(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var uniform: Bool {
+        get {
+            return spine_ik_constraint_data_get_uniform(wrappee) != 0
+        }
+        set {
+            spine_ik_constraint_data_set_uniform(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var mix: Float {
+        get {
+            return spine_ik_constraint_data_get_mix(wrappee)
+        }
+        set {
+            spine_ik_constraint_data_set_mix(wrappee, newValue)
+        }
+    }
+
+    public var softness: Float {
+        get {
+            return spine_ik_constraint_data_get_softness(wrappee)
+        }
+        set {
+            spine_ik_constraint_data_set_softness(wrappee, newValue)
+        }
+    }
+
+}
+
+@objc(SpinePhysicsConstraint)
+@objcMembers
+public final class PhysicsConstraint: NSObject {
+
+    internal let wrappee: spine_physics_constraint
+
+    internal init(_ wrappee: spine_physics_constraint) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var bone: Bone {
+        get {
+            return .init(spine_physics_constraint_get_bone(wrappee))
+        }
+        set {
+            spine_physics_constraint_set_bone(wrappee, newValue.wrappee)
+        }
+    }
+
+    public var inertia: Float {
+        get {
+            return spine_physics_constraint_get_inertia(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_inertia(wrappee, newValue)
+        }
+    }
+
+    public var strength: Float {
+        get {
+            return spine_physics_constraint_get_strength(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_strength(wrappee, newValue)
+        }
+    }
+
+    public var damping: Float {
+        get {
+            return spine_physics_constraint_get_damping(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_damping(wrappee, newValue)
+        }
+    }
+
+    public var massInverse: Float {
+        get {
+            return spine_physics_constraint_get_mass_inverse(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_mass_inverse(wrappee, newValue)
+        }
+    }
+
+    public var wind: Float {
+        get {
+            return spine_physics_constraint_get_wind(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_wind(wrappee, newValue)
+        }
+    }
+
+    public var gravity: Float {
+        get {
+            return spine_physics_constraint_get_gravity(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_gravity(wrappee, newValue)
+        }
+    }
+
+    public var mix: Float {
+        get {
+            return spine_physics_constraint_get_mix(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_mix(wrappee, newValue)
+        }
+    }
+
+    public var reset: Bool {
+        get {
+            return spine_physics_constraint_get_reset(wrappee) != 0
+        }
+        set {
+            spine_physics_constraint_set_reset(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var ux: Float {
+        get {
+            return spine_physics_constraint_get_ux(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_ux(wrappee, newValue)
+        }
+    }
+
+    public var uy: Float {
+        get {
+            return spine_physics_constraint_get_uy(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_uy(wrappee, newValue)
+        }
+    }
+
+    public var cx: Float {
+        get {
+            return spine_physics_constraint_get_cx(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_cx(wrappee, newValue)
+        }
+    }
+
+    public var cy: Float {
+        get {
+            return spine_physics_constraint_get_cy(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_cy(wrappee, newValue)
+        }
+    }
+
+    public var tx: Float {
+        get {
+            return spine_physics_constraint_get_tx(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_tx(wrappee, newValue)
+        }
+    }
+
+    public var ty: Float {
+        get {
+            return spine_physics_constraint_get_ty(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_ty(wrappee, newValue)
+        }
+    }
+
+    public var xOffset: Float {
+        get {
+            return spine_physics_constraint_get_x_offset(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_x_offset(wrappee, newValue)
+        }
+    }
+
+    public var xVelocity: Float {
+        get {
+            return spine_physics_constraint_get_x_velocity(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_x_velocity(wrappee, newValue)
+        }
+    }
+
+    public var yOffset: Float {
+        get {
+            return spine_physics_constraint_get_y_offset(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_y_offset(wrappee, newValue)
+        }
+    }
+
+    public var yVelocity: Float {
+        get {
+            return spine_physics_constraint_get_y_velocity(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_y_velocity(wrappee, newValue)
+        }
+    }
+
+    public var rotateOffset: Float {
+        get {
+            return spine_physics_constraint_get_rotate_offset(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_rotate_offset(wrappee, newValue)
+        }
+    }
+
+    public var rotateVelocity: Float {
+        get {
+            return spine_physics_constraint_get_rotate_velocity(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_rotate_velocity(wrappee, newValue)
+        }
+    }
+
+    public var scaleOffset: Float {
+        get {
+            return spine_physics_constraint_get_scale_offset(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_scale_offset(wrappee, newValue)
+        }
+    }
+
+    public var scaleVelocity: Float {
+        get {
+            return spine_physics_constraint_get_scale_velocity(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_scale_velocity(wrappee, newValue)
+        }
+    }
+
+    public var isActive: Bool {
+        get {
+            return spine_physics_constraint_is_active(wrappee) != 0
+        }
+        set {
+            spine_physics_constraint_set_active(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var remaining: Float {
+        get {
+            return spine_physics_constraint_get_remaining(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_remaining(wrappee, newValue)
+        }
+    }
+
+    public var lastTime: Float {
+        get {
+            return spine_physics_constraint_get_last_time(wrappee)
+        }
+        set {
+            spine_physics_constraint_set_last_time(wrappee, newValue)
+        }
+    }
+
+    public func resetFully() {
+        spine_physics_constraint_reset_fully(wrappee)
+    }
+
+    public func update(physics: Physics) {
+        spine_physics_constraint_update(wrappee, physics)
+    }
+
+    public func translate(x: Float, y: Float) {
+        spine_physics_constraint_translate(wrappee, x, y)
+    }
+
+    public func rotate(x: Float, y: Float, degrees: Float) {
+        spine_physics_constraint_rotate(wrappee, x, y, degrees)
+    }
+
+}
+
+@objc(SpineRegionAttachment)
+@objcMembers
+public final class RegionAttachment: NSObject {
+
+    internal let wrappee: spine_region_attachment
+
+    internal init(_ wrappee: spine_region_attachment) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var color: Color {
+        return .init(spine_region_attachment_get_color(wrappee))
+    }
+
+    public var path: String? {
+        return spine_region_attachment_get_path(wrappee).flatMap { String(cString: $0) }
+    }
+
+    public var region: TextureRegion? {
+        return spine_region_attachment_get_region(wrappee).flatMap { .init($0) }
+    }
+
+    public var sequence: Sequence? {
+        return spine_region_attachment_get_sequence(wrappee).flatMap { .init($0) }
+    }
+
+    public var offset: [Float?] {
+        let num = Int(spine_region_attachment_get_num_offset(wrappee))
+        let ptr = spine_region_attachment_get_offset(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0]
+        }
+    }
+
+    public var uvs: [Float?] {
+        let num = Int(spine_region_attachment_get_num_uvs(wrappee))
+        let ptr = spine_region_attachment_get_uvs(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0]
+        }
+    }
+
+    public var x: Float {
+        get {
+            return spine_region_attachment_get_x(wrappee)
+        }
+        set {
+            spine_region_attachment_set_x(wrappee, newValue)
+        }
+    }
+
+    public var y: Float {
+        get {
+            return spine_region_attachment_get_y(wrappee)
+        }
+        set {
+            spine_region_attachment_set_y(wrappee, newValue)
+        }
+    }
+
+    public var rotation: Float {
+        get {
+            return spine_region_attachment_get_rotation(wrappee)
+        }
+        set {
+            spine_region_attachment_set_rotation(wrappee, newValue)
+        }
+    }
+
+    public var scaleX: Float {
+        get {
+            return spine_region_attachment_get_scale_x(wrappee)
+        }
+        set {
+            spine_region_attachment_set_scale_x(wrappee, newValue)
+        }
+    }
+
+    public var scaleY: Float {
+        get {
+            return spine_region_attachment_get_scale_y(wrappee)
+        }
+        set {
+            spine_region_attachment_set_scale_y(wrappee, newValue)
+        }
+    }
+
+    public var width: Float {
+        get {
+            return spine_region_attachment_get_width(wrappee)
+        }
+        set {
+            spine_region_attachment_set_width(wrappee, newValue)
+        }
+    }
+
+    public var height: Float {
+        get {
+            return spine_region_attachment_get_height(wrappee)
+        }
+        set {
+            spine_region_attachment_set_height(wrappee, newValue)
+        }
+    }
+
+    public func updateRegion() {
+        spine_region_attachment_update_region(wrappee)
+    }
+
+    public func setColor(r: Float, g: Float, b: Float, a: Float) {
+        spine_region_attachment_set_color(wrappee, r, g, b, a)
+    }
+
+}
+
+@objc(SpineVertexAttachment)
+@objcMembers
+public final class VertexAttachment: NSObject {
+
+    internal let wrappee: spine_vertex_attachment
+
+    internal init(_ wrappee: spine_vertex_attachment) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var worldVerticesLength: Int32 {
+        return spine_vertex_attachment_get_world_vertices_length(wrappee)
+    }
+
+    public var bones: [Int32?] {
+        let num = Int(spine_vertex_attachment_get_num_bones(wrappee))
+        let ptr = spine_vertex_attachment_get_bones(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0]
+        }
+    }
+
+    public var vertices: [Float?] {
+        let num = Int(spine_vertex_attachment_get_num_vertices(wrappee))
+        let ptr = spine_vertex_attachment_get_vertices(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0]
+        }
+    }
+
+    public var timelineAttachment: Attachment? {
+        get {
+            return spine_vertex_attachment_get_timeline_attachment(wrappee).flatMap { .init($0) }
+        }
+        set {
+            spine_vertex_attachment_set_timeline_attachment(wrappee, newValue?.wrappee)
+        }
+    }
+
+}
+
+@objc(SpineSkeletonDrawable)
+@objcMembers
+public final class SkeletonDrawable: NSObject {
+
+    internal let wrappee: spine_skeleton_drawable
+    internal var disposed = false
+
+    internal init(_ wrappee: spine_skeleton_drawable) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var skeleton: Skeleton {
+        return .init(spine_skeleton_drawable_get_skeleton(wrappee))
+    }
+
+    public var animationState: AnimationState {
+        return .init(spine_skeleton_drawable_get_animation_state(wrappee))
+    }
+
+    public var animationStateData: AnimationStateData {
+        return .init(spine_skeleton_drawable_get_animation_state_data(wrappee))
+    }
+
+    public var animationStateEvents: AnimationStateEvents {
+        return .init(spine_skeleton_drawable_get_animation_state_events(wrappee))
+    }
+
+    public func dispose() {
+        if disposed { return }
+        disposed = true
+        spine_skeleton_drawable_dispose(wrappee)
+    }
+
+}
+
+@objc(SpinePointAttachment)
+@objcMembers
+public final class PointAttachment: NSObject {
+
+    internal let wrappee: spine_point_attachment
+
+    internal init(_ wrappee: spine_point_attachment) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var color: Color {
+        return .init(spine_point_attachment_get_color(wrappee))
+    }
+
+    public var x: Float {
+        get {
+            return spine_point_attachment_get_x(wrappee)
+        }
+        set {
+            spine_point_attachment_set_x(wrappee, newValue)
+        }
+    }
+
+    public var y: Float {
+        get {
+            return spine_point_attachment_get_y(wrappee)
+        }
+        set {
+            spine_point_attachment_set_y(wrappee, newValue)
+        }
+    }
+
+    public var rotation: Float {
+        get {
+            return spine_point_attachment_get_rotation(wrappee)
+        }
+        set {
+            spine_point_attachment_set_rotation(wrappee, newValue)
+        }
+    }
+
+    @discardableResult
+    public func computeWorldPosition(bone: Bone) -> Vector {
+        return .init(spine_point_attachment_compute_world_position(wrappee, bone.wrappee))
+    }
+
+    @discardableResult
+    public func computeWorldRotation(bone: Bone) -> Float {
+        return spine_point_attachment_compute_world_rotation(wrappee, bone.wrappee)
+    }
+
+    public func setColor(r: Float, g: Float, b: Float, a: Float) {
+        spine_point_attachment_set_color(wrappee, r, g, b, a)
+    }
+
+}
+
+@objc(SpineMeshAttachment)
+@objcMembers
+public final class MeshAttachment: NSObject {
+
+    internal let wrappee: spine_mesh_attachment
+
+    internal init(_ wrappee: spine_mesh_attachment) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var regionUvs: [Float?] {
+        let num = Int(spine_mesh_attachment_get_num_region_uvs(wrappee))
+        let ptr = spine_mesh_attachment_get_region_uvs(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0]
+        }
+    }
+
+    public var uvs: [Float?] {
+        let num = Int(spine_mesh_attachment_get_num_uvs(wrappee))
+        let ptr = spine_mesh_attachment_get_uvs(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0]
+        }
+    }
+
+    public var triangles: [UInt16] {
+        let num = Int(spine_mesh_attachment_get_num_triangles(wrappee))
+        let ptr = spine_mesh_attachment_get_triangles(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0]
+        }
+    }
+
+    public var color: Color {
+        return .init(spine_mesh_attachment_get_color(wrappee))
+    }
+
+    public var path: String? {
+        return spine_mesh_attachment_get_path(wrappee).flatMap { String(cString: $0) }
+    }
+
+    public var region: TextureRegion {
+        return .init(spine_mesh_attachment_get_region(wrappee))
+    }
+
+    public var sequence: Sequence? {
+        return spine_mesh_attachment_get_sequence(wrappee).flatMap { .init($0) }
+    }
+
+    public var edges: [UInt16] {
+        let num = Int(spine_mesh_attachment_get_num_edges(wrappee))
+        let ptr = spine_mesh_attachment_get_edges(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0]
+        }
+    }
+
+    public var hullLength: Int32 {
+        get {
+            return spine_mesh_attachment_get_hull_length(wrappee)
+        }
+        set {
+            spine_mesh_attachment_set_hull_length(wrappee, newValue)
+        }
+    }
+
+    public var parentMesh: MeshAttachment? {
+        get {
+            return spine_mesh_attachment_get_parent_mesh(wrappee).flatMap { .init($0) }
+        }
+        set {
+            spine_mesh_attachment_set_parent_mesh(wrappee, newValue?.wrappee)
+        }
+    }
+
+    public var width: Float {
+        get {
+            return spine_mesh_attachment_get_width(wrappee)
+        }
+        set {
+            spine_mesh_attachment_set_width(wrappee, newValue)
+        }
+    }
+
+    public var height: Float {
+        get {
+            return spine_mesh_attachment_get_height(wrappee)
+        }
+        set {
+            spine_mesh_attachment_set_height(wrappee, newValue)
+        }
+    }
+
+    public func updateRegion() {
+        spine_mesh_attachment_update_region(wrappee)
+    }
+
+    public func setColor(r: Float, g: Float, b: Float, a: Float) {
+        spine_mesh_attachment_set_color(wrappee, r, g, b, a)
+    }
+
+}
+
+@objc(SpinePathAttachment)
+@objcMembers
+public final class PathAttachment: NSObject {
+
+    internal let wrappee: spine_path_attachment
+
+    internal init(_ wrappee: spine_path_attachment) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var lengths: [Float?] {
+        let num = Int(spine_path_attachment_get_num_lengths(wrappee))
+        let ptr = spine_path_attachment_get_lengths(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0]
+        }
+    }
+
+    public var color: Color {
+        return .init(spine_path_attachment_get_color(wrappee))
+    }
+
+    public var isClosed: Bool {
+        get {
+            return spine_path_attachment_get_is_closed(wrappee) != 0
+        }
+        set {
+            spine_path_attachment_set_is_closed(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var isConstantSpeed: Bool {
+        get {
+            return spine_path_attachment_get_is_constant_speed(wrappee) != 0
+        }
+        set {
+            spine_path_attachment_set_is_constant_speed(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public func setColor(r: Float, g: Float, b: Float, a: Float) {
+        spine_path_attachment_set_color(wrappee, r, g, b, a)
+    }
+
+}
+
+@objc(SpineConstraintData)
+@objcMembers
+public final class ConstraintData: NSObject {
+
+    internal let wrappee: spine_constraint_data
+
+    internal init(_ wrappee: spine_constraint_data) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var type: ConstraintType {
+        return spine_constraint_data_get_type(wrappee)
+    }
+
+    public var name: String? {
+        return spine_constraint_data_get_name(wrappee).flatMap { String(cString: $0) }
+    }
+
+    public var order: UInt64 {
+        get {
+            return spine_constraint_data_get_order(wrappee)
+        }
+        set {
+            spine_constraint_data_set_order(wrappee, newValue)
+        }
+    }
+
+    public var isSkinRequired: Bool {
+        get {
+            return spine_constraint_data_get_is_skin_required(wrappee) != 0
+        }
+        set {
+            spine_constraint_data_set_is_skin_required(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+}
+
+@objc(SpinePathConstraint)
+@objcMembers
+public final class PathConstraint: NSObject {
+
+    internal let wrappee: spine_path_constraint
+
+    internal init(_ wrappee: spine_path_constraint) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var order: Int32 {
+        return spine_path_constraint_get_order(wrappee)
+    }
+
+    public var data: PathConstraintData {
+        return .init(spine_path_constraint_get_data(wrappee))
+    }
+
+    public var bones: [Bone] {
+        let num = Int(spine_path_constraint_get_num_bones(wrappee))
+        let ptr = spine_path_constraint_get_bones(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var target: Slot {
+        get {
+            return .init(spine_path_constraint_get_target(wrappee))
+        }
+        set {
+            spine_path_constraint_set_target(wrappee, newValue.wrappee)
+        }
+    }
+
+    public var position: Float {
+        get {
+            return spine_path_constraint_get_position(wrappee)
+        }
+        set {
+            spine_path_constraint_set_position(wrappee, newValue)
+        }
+    }
+
+    public var spacing: Float {
+        get {
+            return spine_path_constraint_get_spacing(wrappee)
+        }
+        set {
+            spine_path_constraint_set_spacing(wrappee, newValue)
+        }
+    }
+
+    public var mixRotate: Float {
+        get {
+            return spine_path_constraint_get_mix_rotate(wrappee)
+        }
+        set {
+            spine_path_constraint_set_mix_rotate(wrappee, newValue)
+        }
+    }
+
+    public var mixX: Float {
+        get {
+            return spine_path_constraint_get_mix_x(wrappee)
+        }
+        set {
+            spine_path_constraint_set_mix_x(wrappee, newValue)
+        }
+    }
+
+    public var mixY: Float {
+        get {
+            return spine_path_constraint_get_mix_y(wrappee)
+        }
+        set {
+            spine_path_constraint_set_mix_y(wrappee, newValue)
+        }
+    }
+
+    public var isActive: Bool {
+        get {
+            return spine_path_constraint_get_is_active(wrappee) != 0
+        }
+        set {
+            spine_path_constraint_set_is_active(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public func update() {
+        spine_path_constraint_update(wrappee)
+    }
+
+}
+
+@objc(SpineAnimationState)
+@objcMembers
+public final class AnimationState: NSObject {
+
+    internal let wrappee: spine_animation_state
+
+    internal init(_ wrappee: spine_animation_state) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var data: AnimationStateData {
+        return .init(spine_animation_state_get_data(wrappee))
+    }
+
+    public var timeScale: Float {
+        get {
+            return spine_animation_state_get_time_scale(wrappee)
+        }
+        set {
+            spine_animation_state_set_time_scale(wrappee, newValue)
+        }
+    }
+
+    public func update(delta: Float) {
+        spine_animation_state_update(wrappee, delta)
+    }
+
+    public func apply(skeleton: Skeleton) {
+        spine_animation_state_apply(wrappee, skeleton.wrappee)
+    }
+
+    public func clearTracks() {
+        spine_animation_state_clear_tracks(wrappee)
+    }
+
+    public func clearTrack(trackIndex: Int32) {
+        spine_animation_state_clear_track(wrappee, trackIndex)
+    }
+
+    @discardableResult
+    public func setAnimationByName(trackIndex: Int32, animationName: String?, loop: Bool) -> TrackEntry {
+        return .init(spine_animation_state_set_animation_by_name(wrappee, trackIndex, animationName, loop ? -1 : 0))
+    }
+
+    @discardableResult
+    public func setAnimation(trackIndex: Int32, animation: Animation, loop: Bool) -> TrackEntry {
+        return .init(spine_animation_state_set_animation(wrappee, trackIndex, animation.wrappee, loop ? -1 : 0))
+    }
+
+    @discardableResult
+    public func addAnimationByName(trackIndex: Int32, animationName: String?, loop: Bool, delay: Float) -> TrackEntry {
+        return .init(spine_animation_state_add_animation_by_name(wrappee, trackIndex, animationName, loop ? -1 : 0, delay))
+    }
+
+    @discardableResult
+    public func addAnimation(trackIndex: Int32, animation: Animation, loop: Bool, delay: Float) -> TrackEntry {
+        return .init(spine_animation_state_add_animation(wrappee, trackIndex, animation.wrappee, loop ? -1 : 0, delay))
+    }
+
+    @discardableResult
+    public func setEmptyAnimation(trackIndex: Int32, mixDuration: Float) -> TrackEntry {
+        return .init(spine_animation_state_set_empty_animation(wrappee, trackIndex, mixDuration))
+    }
+
+    @discardableResult
+    public func addEmptyAnimation(trackIndex: Int32, mixDuration: Float, delay: Float) -> TrackEntry {
+        return .init(spine_animation_state_add_empty_animation(wrappee, trackIndex, mixDuration, delay))
+    }
+
+    @discardableResult
+    public func getCurrent(trackIndex: Int32) -> TrackEntry? {
+        return spine_animation_state_get_current(wrappee, trackIndex).flatMap { .init($0) }
+    }
+
+    public func setEmptyAnimations(mixDuration: Float) {
+        spine_animation_state_set_empty_animations(wrappee, mixDuration)
+    }
+
+}
+
+@objc(SpineTextureRegion)
+@objcMembers
+public final class TextureRegion: NSObject {
+
+    internal let wrappee: spine_texture_region
+
+    internal init(_ wrappee: spine_texture_region) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var texture: UnsafeMutableRawPointer {
+        get {
+            return spine_texture_region_get_texture(wrappee)
+        }
+        set {
+            spine_texture_region_set_texture(wrappee, newValue)
+        }
+    }
+
+    public var u: Float {
+        get {
+            return spine_texture_region_get_u(wrappee)
+        }
+        set {
+            spine_texture_region_set_u(wrappee, newValue)
+        }
+    }
+
+    public var v: Float {
+        get {
+            return spine_texture_region_get_v(wrappee)
+        }
+        set {
+            spine_texture_region_set_v(wrappee, newValue)
+        }
+    }
+
+    public var u2: Float {
+        get {
+            return spine_texture_region_get_u2(wrappee)
+        }
+        set {
+            spine_texture_region_set_u2(wrappee, newValue)
+        }
+    }
+
+    public var v2: Float {
+        get {
+            return spine_texture_region_get_v2(wrappee)
+        }
+        set {
+            spine_texture_region_set_v2(wrappee, newValue)
+        }
+    }
+
+    public var degrees: Int32 {
+        get {
+            return spine_texture_region_get_degrees(wrappee)
+        }
+        set {
+            spine_texture_region_set_degrees(wrappee, newValue)
+        }
+    }
+
+    public var offsetX: Float {
+        get {
+            return spine_texture_region_get_offset_x(wrappee)
+        }
+        set {
+            spine_texture_region_set_offset_x(wrappee, newValue)
+        }
+    }
+
+    public var offsetY: Float {
+        get {
+            return spine_texture_region_get_offset_y(wrappee)
+        }
+        set {
+            spine_texture_region_set_offset_y(wrappee, newValue)
+        }
+    }
+
+    public var width: Int32 {
+        get {
+            return spine_texture_region_get_width(wrappee)
+        }
+        set {
+            spine_texture_region_set_width(wrappee, newValue)
+        }
+    }
+
+    public var height: Int32 {
+        get {
+            return spine_texture_region_get_height(wrappee)
+        }
+        set {
+            spine_texture_region_set_height(wrappee, newValue)
+        }
+    }
+
+    public var originalWidth: Int32 {
+        get {
+            return spine_texture_region_get_original_width(wrappee)
+        }
+        set {
+            spine_texture_region_set_original_width(wrappee, newValue)
+        }
+    }
+
+    public var originalHeight: Int32 {
+        get {
+            return spine_texture_region_get_original_height(wrappee)
+        }
+        set {
+            spine_texture_region_set_original_height(wrappee, newValue)
+        }
+    }
+
+}
+
+@objc(SpineRenderCommand)
+@objcMembers
+public final class RenderCommand: NSObject {
+
+    internal let wrappee: spine_render_command
+
+    internal init(_ wrappee: spine_render_command) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var indices: [UInt16] {
+        let num = Int(spine_render_command_get_num_indices(wrappee))
+        let ptr = spine_render_command_get_indices(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0]
+        }
+    }
+
+    public var atlasPage: Int32 {
+        return spine_render_command_get_atlas_page(wrappee)
+    }
+
+    public var blendMode: BlendMode {
+        return spine_render_command_get_blend_mode(wrappee)
+    }
+
+    public var next: RenderCommand {
+        return .init(spine_render_command_get_next(wrappee))
+    }
+
+}
+
+@objc(SpineSkeletonData)
+@objcMembers
+public final class SkeletonData: NSObject {
+
+    internal let wrappee: spine_skeleton_data
+    internal var disposed = false
+
+    internal init(_ wrappee: spine_skeleton_data) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var name: String? {
+        return spine_skeleton_data_get_name(wrappee).flatMap { String(cString: $0) }
+    }
+
+    public var bones: [BoneData] {
+        let num = Int(spine_skeleton_data_get_num_bones(wrappee))
+        let ptr = spine_skeleton_data_get_bones(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var slots: [SlotData] {
+        let num = Int(spine_skeleton_data_get_num_slots(wrappee))
+        let ptr = spine_skeleton_data_get_slots(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var skins: [Skin] {
+        let num = Int(spine_skeleton_data_get_num_skins(wrappee))
+        let ptr = spine_skeleton_data_get_skins(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var events: [EventData] {
+        let num = Int(spine_skeleton_data_get_num_events(wrappee))
+        let ptr = spine_skeleton_data_get_events(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var animations: [Animation] {
+        let num = Int(spine_skeleton_data_get_num_animations(wrappee))
+        let ptr = spine_skeleton_data_get_animations(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var ikConstraints: [IkConstraintData] {
+        let num = Int(spine_skeleton_data_get_num_ik_constraints(wrappee))
+        let ptr = spine_skeleton_data_get_ik_constraints(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var transformConstraints: [TransformConstraintData] {
+        let num = Int(spine_skeleton_data_get_num_transform_constraints(wrappee))
+        let ptr = spine_skeleton_data_get_transform_constraints(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var pathConstraints: [PathConstraintData] {
+        let num = Int(spine_skeleton_data_get_num_path_constraints(wrappee))
+        let ptr = spine_skeleton_data_get_path_constraints(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var physicsConstraints: [PhysicsConstraintData] {
+        let num = Int(spine_skeleton_data_get_num_physics_constraints(wrappee))
+        let ptr = spine_skeleton_data_get_physics_constraints(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var version: String? {
+        return spine_skeleton_data_get_version(wrappee).flatMap { String(cString: $0) }
+    }
+
+    public var imagesPath: String? {
+        return spine_skeleton_data_get_images_path(wrappee).flatMap { String(cString: $0) }
+    }
+
+    public var audioPath: String? {
+        return spine_skeleton_data_get_audio_path(wrappee).flatMap { String(cString: $0) }
+    }
+
+    public var fps: Float {
+        return spine_skeleton_data_get_fps(wrappee)
+    }
+
+    public var referenceScale: Float {
+        return spine_skeleton_data_get_reference_scale(wrappee)
+    }
+
+    public var defaultSkin: Skin? {
+        get {
+            return spine_skeleton_data_get_default_skin(wrappee).flatMap { .init($0) }
+        }
+        set {
+            spine_skeleton_data_set_default_skin(wrappee, newValue?.wrappee)
+        }
+    }
+
+    public var x: Float {
+        get {
+            return spine_skeleton_data_get_x(wrappee)
+        }
+        set {
+            spine_skeleton_data_set_x(wrappee, newValue)
+        }
+    }
+
+    public var y: Float {
+        get {
+            return spine_skeleton_data_get_y(wrappee)
+        }
+        set {
+            spine_skeleton_data_set_y(wrappee, newValue)
+        }
+    }
+
+    public var width: Float {
+        get {
+            return spine_skeleton_data_get_width(wrappee)
+        }
+        set {
+            spine_skeleton_data_set_width(wrappee, newValue)
+        }
+    }
+
+    public var height: Float {
+        get {
+            return spine_skeleton_data_get_height(wrappee)
+        }
+        set {
+            spine_skeleton_data_set_height(wrappee, newValue)
+        }
+    }
+
+    @discardableResult
+    public func findBone(name: String?) -> BoneData? {
+        return spine_skeleton_data_find_bone(wrappee, name).flatMap { .init($0) }
+    }
+
+    @discardableResult
+    public func findSlot(name: String?) -> SlotData? {
+        return spine_skeleton_data_find_slot(wrappee, name).flatMap { .init($0) }
+    }
+
+    @discardableResult
+    public func findSkin(name: String?) -> Skin? {
+        return spine_skeleton_data_find_skin(wrappee, name).flatMap { .init($0) }
+    }
+
+    @discardableResult
+    public func findEvent(name: String?) -> EventData? {
+        return spine_skeleton_data_find_event(wrappee, name).flatMap { .init($0) }
+    }
+
+    @discardableResult
+    public func findAnimation(name: String?) -> Animation? {
+        return spine_skeleton_data_find_animation(wrappee, name).flatMap { .init($0) }
+    }
+
+    @discardableResult
+    public func findIkConstraint(name: String?) -> IkConstraintData? {
+        return spine_skeleton_data_find_ik_constraint(wrappee, name).flatMap { .init($0) }
+    }
+
+    @discardableResult
+    public func findTransformConstraint(name: String?) -> TransformConstraintData? {
+        return spine_skeleton_data_find_transform_constraint(wrappee, name).flatMap { .init($0) }
+    }
+
+    @discardableResult
+    public func findPathConstraint(name: String?) -> PathConstraintData? {
+        return spine_skeleton_data_find_path_constraint(wrappee, name).flatMap { .init($0) }
+    }
+
+    @discardableResult
+    public func findPhysicsConstraint(name: String?) -> PhysicsConstraintData? {
+        return spine_skeleton_data_find_physics_constraint(wrappee, name).flatMap { .init($0) }
+    }
+
+    public func dispose() {
+        if disposed { return }
+        disposed = true
+        spine_skeleton_data_dispose(wrappee)
+    }
+
+}
+
+@objc(SpineIkConstraint)
+@objcMembers
+public final class IkConstraint: NSObject {
+
+    internal let wrappee: spine_ik_constraint
+
+    internal init(_ wrappee: spine_ik_constraint) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var order: Int32 {
+        return spine_ik_constraint_get_order(wrappee)
+    }
+
+    public var data: IkConstraintData {
+        return .init(spine_ik_constraint_get_data(wrappee))
+    }
+
+    public var bones: [Bone] {
+        let num = Int(spine_ik_constraint_get_num_bones(wrappee))
+        let ptr = spine_ik_constraint_get_bones(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var target: Bone {
+        get {
+            return .init(spine_ik_constraint_get_target(wrappee))
+        }
+        set {
+            spine_ik_constraint_set_target(wrappee, newValue.wrappee)
+        }
+    }
+
+    public var bendDirection: Int32 {
+        get {
+            return spine_ik_constraint_get_bend_direction(wrappee)
+        }
+        set {
+            spine_ik_constraint_set_bend_direction(wrappee, newValue)
+        }
+    }
+
+    public var compress: Bool {
+        get {
+            return spine_ik_constraint_get_compress(wrappee) != 0
+        }
+        set {
+            spine_ik_constraint_set_compress(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var stretch: Bool {
+        get {
+            return spine_ik_constraint_get_stretch(wrappee) != 0
+        }
+        set {
+            spine_ik_constraint_set_stretch(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var mix: Float {
+        get {
+            return spine_ik_constraint_get_mix(wrappee)
+        }
+        set {
+            spine_ik_constraint_set_mix(wrappee, newValue)
+        }
+    }
+
+    public var softness: Float {
+        get {
+            return spine_ik_constraint_get_softness(wrappee)
+        }
+        set {
+            spine_ik_constraint_set_softness(wrappee, newValue)
+        }
+    }
+
+    public var isActive: Bool {
+        get {
+            return spine_ik_constraint_get_is_active(wrappee) != 0
+        }
+        set {
+            spine_ik_constraint_set_is_active(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public func update() {
+        spine_ik_constraint_update(wrappee)
+    }
+
+}
+
+@objc(SpineSkinEntries)
+@objcMembers
+public final class SkinEntries: NSObject {
+
+    internal let wrappee: spine_skin_entries
+    internal var disposed = false
+
+    internal init(_ wrappee: spine_skin_entries) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    @discardableResult
+    public func getEntry(index: Int32) -> SkinEntry {
+        return .init(spine_skin_entries_get_entry(wrappee, index))
+    }
+
+    public func dispose() {
+        if disposed { return }
+        disposed = true
+        spine_skin_entries_dispose(wrappee)
+    }
+
+}
+
+@objc(SpineTrackEntry)
+@objcMembers
+public final class TrackEntry: NSObject {
+
+    internal let wrappee: spine_track_entry
+
+    internal init(_ wrappee: spine_track_entry) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var trackIndex: Int32 {
+        return spine_track_entry_get_track_index(wrappee)
+    }
+
+    public var animation: Animation {
+        return .init(spine_track_entry_get_animation(wrappee))
+    }
+
+    public var previous: TrackEntry {
+        return .init(spine_track_entry_get_previous(wrappee))
+    }
+
+    public var animationTime: Float {
+        return spine_track_entry_get_animation_time(wrappee)
+    }
+
+    public var next: TrackEntry? {
+        return spine_track_entry_get_next(wrappee).flatMap { .init($0) }
+    }
+
+    public var isComplete: Bool {
+        return spine_track_entry_is_complete(wrappee) != 0
+    }
+
+    public var mixingFrom: TrackEntry? {
+        return spine_track_entry_get_mixing_from(wrappee).flatMap { .init($0) }
+    }
+
+    public var mixingTo: TrackEntry? {
+        return spine_track_entry_get_mixing_to(wrappee).flatMap { .init($0) }
+    }
+
+    public var trackComplete: Float {
+        return spine_track_entry_get_track_complete(wrappee)
+    }
+
+    public var isNextReady: Bool {
+        return spine_track_entry_is_next_ready(wrappee) != 0
+    }
+
+    public var loop: Bool {
+        get {
+            return spine_track_entry_get_loop(wrappee) != 0
+        }
+        set {
+            spine_track_entry_set_loop(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var holdPrevious: Bool {
+        get {
+            return spine_track_entry_get_hold_previous(wrappee) != 0
+        }
+        set {
+            spine_track_entry_set_hold_previous(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var reverse: Bool {
+        get {
+            return spine_track_entry_get_reverse(wrappee) != 0
+        }
+        set {
+            spine_track_entry_set_reverse(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var shortestRotation: Bool {
+        get {
+            return spine_track_entry_get_shortest_rotation(wrappee) != 0
+        }
+        set {
+            spine_track_entry_set_shortest_rotation(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var delay: Float {
+        get {
+            return spine_track_entry_get_delay(wrappee)
+        }
+        set {
+            spine_track_entry_set_delay(wrappee, newValue)
+        }
+    }
+
+    public var trackTime: Float {
+        get {
+            return spine_track_entry_get_track_time(wrappee)
+        }
+        set {
+            spine_track_entry_set_track_time(wrappee, newValue)
+        }
+    }
+
+    public var trackEnd: Float {
+        get {
+            return spine_track_entry_get_track_end(wrappee)
+        }
+        set {
+            spine_track_entry_set_track_end(wrappee, newValue)
+        }
+    }
+
+    public var animationStart: Float {
+        get {
+            return spine_track_entry_get_animation_start(wrappee)
+        }
+        set {
+            spine_track_entry_set_animation_start(wrappee, newValue)
+        }
+    }
+
+    public var animationEnd: Float {
+        get {
+            return spine_track_entry_get_animation_end(wrappee)
+        }
+        set {
+            spine_track_entry_set_animation_end(wrappee, newValue)
+        }
+    }
+
+    public var animationLast: Float {
+        get {
+            return spine_track_entry_get_animation_last(wrappee)
+        }
+        set {
+            spine_track_entry_set_animation_last(wrappee, newValue)
+        }
+    }
+
+    public var timeScale: Float {
+        get {
+            return spine_track_entry_get_time_scale(wrappee)
+        }
+        set {
+            spine_track_entry_set_time_scale(wrappee, newValue)
+        }
+    }
+
+    public var alpha: Float {
+        get {
+            return spine_track_entry_get_alpha(wrappee)
+        }
+        set {
+            spine_track_entry_set_alpha(wrappee, newValue)
+        }
+    }
+
+    public var eventThreshold: Float {
+        get {
+            return spine_track_entry_get_event_threshold(wrappee)
+        }
+        set {
+            spine_track_entry_set_event_threshold(wrappee, newValue)
+        }
+    }
+
+    public var alphaAttachmentThreshold: Float {
+        get {
+            return spine_track_entry_get_alpha_attachment_threshold(wrappee)
+        }
+        set {
+            spine_track_entry_set_alpha_attachment_threshold(wrappee, newValue)
+        }
+    }
+
+    public var mixAttachmentThreshold: Float {
+        get {
+            return spine_track_entry_get_mix_attachment_threshold(wrappee)
+        }
+        set {
+            spine_track_entry_set_mix_attachment_threshold(wrappee, newValue)
+        }
+    }
+
+    public var mixDrawOrderThreshold: Float {
+        get {
+            return spine_track_entry_get_mix_draw_order_threshold(wrappee)
+        }
+        set {
+            spine_track_entry_set_mix_draw_order_threshold(wrappee, newValue)
+        }
+    }
+
+    public var mixTime: Float {
+        get {
+            return spine_track_entry_get_mix_time(wrappee)
+        }
+        set {
+            spine_track_entry_set_mix_time(wrappee, newValue)
+        }
+    }
+
+    public var mixDuration: Float {
+        get {
+            return spine_track_entry_get_mix_duration(wrappee)
+        }
+        set {
+            spine_track_entry_set_mix_duration(wrappee, newValue)
+        }
+    }
+
+    public var mixBlend: MixBlend {
+        get {
+            return spine_track_entry_get_mix_blend(wrappee)
+        }
+        set {
+            spine_track_entry_set_mix_blend(wrappee, newValue)
+        }
+    }
+
+    public func resetRotationDirections() {
+        spine_track_entry_reset_rotation_directions(wrappee)
+    }
+
+    @discardableResult
+    public func wasApplied() -> Bool {
+        return spine_track_entry_was_applied(wrappee) != 0
+    }
+
+}
+
+@objc(SpineAttachment)
+@objcMembers
+public final class Attachment: NSObject {
+
+    internal let wrappee: spine_attachment
+    internal var disposed = false
+
+    internal init(_ wrappee: spine_attachment) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var name: String? {
+        return spine_attachment_get_name(wrappee).flatMap { String(cString: $0) }
+    }
+
+    public var type: AttachmentType {
+        return spine_attachment_get_type(wrappee)
+    }
+
+    public func dispose() {
+        if disposed { return }
+        disposed = true
+        spine_attachment_dispose(wrappee)
+    }
+
+}
+
+@objc(SpineConstraint)
+@objcMembers
+public final class Constraint: NSObject {
+
+    internal let wrappee: spine_constraint
+
+    internal init(_ wrappee: spine_constraint) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+}
+
+@objc(SpineEventData)
+@objcMembers
+public final class EventData: NSObject {
+
+    internal let wrappee: spine_event_data
+
+    internal init(_ wrappee: spine_event_data) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var name: String? {
+        return spine_event_data_get_name(wrappee).flatMap { String(cString: $0) }
+    }
+
+    public var audioPath: String? {
+        return spine_event_data_get_audio_path(wrappee).flatMap { String(cString: $0) }
+    }
+
+    public var intValue: Int32 {
+        get {
+            return spine_event_data_get_int_value(wrappee)
+        }
+        set {
+            spine_event_data_set_int_value(wrappee, newValue)
+        }
+    }
+
+    public var floatValue: Float {
+        get {
+            return spine_event_data_get_float_value(wrappee)
+        }
+        set {
+            spine_event_data_set_float_value(wrappee, newValue)
+        }
+    }
+
+    public var stringValue: String? {
+        get {
+            return spine_event_data_get_string_value(wrappee).flatMap { String(cString: $0) }
+        }
+        set {
+            spine_event_data_set_string_value(wrappee, newValue)
+        }
+    }
+
+    public var volume: Float {
+        get {
+            return spine_event_data_get_volume(wrappee)
+        }
+        set {
+            spine_event_data_set_volume(wrappee, newValue)
+        }
+    }
+
+    public var balance: Float {
+        get {
+            return spine_event_data_get_balance(wrappee)
+        }
+        set {
+            spine_event_data_set_balance(wrappee, newValue)
+        }
+    }
+
+}
+
+@objc(SpineSkinEntry)
+@objcMembers
+public final class SkinEntry: NSObject {
+
+    internal let wrappee: spine_skin_entry
+
+    internal init(_ wrappee: spine_skin_entry) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var slotIndex: Int32 {
+        return spine_skin_entry_get_slot_index(wrappee)
+    }
+
+    public var name: String? {
+        return spine_skin_entry_get_name(wrappee).flatMap { String(cString: $0) }
+    }
+
+    public var attachment: Attachment {
+        return .init(spine_skin_entry_get_attachment(wrappee))
+    }
+
+}
+
+@objc(SpineBoneData)
+@objcMembers
+public final class BoneData: NSObject {
+
+    internal let wrappee: spine_bone_data
+
+    internal init(_ wrappee: spine_bone_data) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var index: Int32 {
+        return spine_bone_data_get_index(wrappee)
+    }
+
+    public var name: String? {
+        return spine_bone_data_get_name(wrappee).flatMap { String(cString: $0) }
+    }
+
+    public var parent: BoneData? {
+        return spine_bone_data_get_parent(wrappee).flatMap { .init($0) }
+    }
+
+    public var color: Color {
+        return .init(spine_bone_data_get_color(wrappee))
+    }
+
+    public var length: Float {
+        get {
+            return spine_bone_data_get_length(wrappee)
+        }
+        set {
+            spine_bone_data_set_length(wrappee, newValue)
+        }
+    }
+
+    public var x: Float {
+        get {
+            return spine_bone_data_get_x(wrappee)
+        }
+        set {
+            spine_bone_data_set_x(wrappee, newValue)
+        }
+    }
+
+    public var y: Float {
+        get {
+            return spine_bone_data_get_y(wrappee)
+        }
+        set {
+            spine_bone_data_set_y(wrappee, newValue)
+        }
+    }
+
+    public var rotation: Float {
+        get {
+            return spine_bone_data_get_rotation(wrappee)
+        }
+        set {
+            spine_bone_data_set_rotation(wrappee, newValue)
+        }
+    }
+
+    public var scaleX: Float {
+        get {
+            return spine_bone_data_get_scale_x(wrappee)
+        }
+        set {
+            spine_bone_data_set_scale_x(wrappee, newValue)
+        }
+    }
+
+    public var scaleY: Float {
+        get {
+            return spine_bone_data_get_scale_y(wrappee)
+        }
+        set {
+            spine_bone_data_set_scale_y(wrappee, newValue)
+        }
+    }
+
+    public var shearX: Float {
+        get {
+            return spine_bone_data_get_shear_x(wrappee)
+        }
+        set {
+            spine_bone_data_set_shear_x(wrappee, newValue)
+        }
+    }
+
+    public var shearY: Float {
+        get {
+            return spine_bone_data_get_shear_y(wrappee)
+        }
+        set {
+            spine_bone_data_set_shear_y(wrappee, newValue)
+        }
+    }
+
+    public var inherit: Inherit {
+        get {
+            return spine_bone_data_get_inherit(wrappee)
+        }
+        set {
+            spine_bone_data_set_inherit(wrappee, newValue)
+        }
+    }
+
+    public var isSkinRequired: Bool {
+        get {
+            return spine_bone_data_get_is_skin_required(wrappee) != 0
+        }
+        set {
+            spine_bone_data_set_is_skin_required(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var isVisible: Bool {
+        get {
+            return spine_bone_data_is_visible(wrappee) != 0
+        }
+        set {
+            spine_bone_data_set_visible(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public func setColor(r: Float, g: Float, b: Float, a: Float) {
+        spine_bone_data_set_color(wrappee, r, g, b, a)
+    }
+
+}
+
+@objc(SpineSlotData)
+@objcMembers
+public final class SlotData: NSObject {
+
+    internal let wrappee: spine_slot_data
+
+    internal init(_ wrappee: spine_slot_data) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var index: Int32 {
+        return spine_slot_data_get_index(wrappee)
+    }
+
+    public var name: String? {
+        return spine_slot_data_get_name(wrappee).flatMap { String(cString: $0) }
+    }
+
+    public var boneData: BoneData {
+        return .init(spine_slot_data_get_bone_data(wrappee))
+    }
+
+    public var color: Color {
+        return .init(spine_slot_data_get_color(wrappee))
+    }
+
+    public var darkColor: Color {
+        return .init(spine_slot_data_get_dark_color(wrappee))
+    }
+
+    public var hasDarkColor: Bool {
+        get {
+            return spine_slot_data_get_has_dark_color(wrappee) != 0
+        }
+        set {
+            spine_slot_data_set_has_dark_color(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var attachmentName: String? {
+        get {
+            return spine_slot_data_get_attachment_name(wrappee).flatMap { String(cString: $0) }
+        }
+        set {
+            spine_slot_data_set_attachment_name(wrappee, newValue)
+        }
+    }
+
+    public var blendMode: BlendMode {
+        get {
+            return spine_slot_data_get_blend_mode(wrappee)
+        }
+        set {
+            spine_slot_data_set_blend_mode(wrappee, newValue)
+        }
+    }
+
+    public var isVisible: Bool {
+        get {
+            return spine_slot_data_is_visible(wrappee) != 0
+        }
+        set {
+            spine_slot_data_set_visible(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public func setColor(r: Float, g: Float, b: Float, a: Float) {
+        spine_slot_data_set_color(wrappee, r, g, b, a)
+    }
+
+    public func setDarkColor(r: Float, g: Float, b: Float, a: Float) {
+        spine_slot_data_set_dark_color(wrappee, r, g, b, a)
+    }
+
+}
+
+@objc(SpineAnimation)
+@objcMembers
+public final class Animation: NSObject {
+
+    internal let wrappee: spine_animation
+
+    internal init(_ wrappee: spine_animation) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var name: String? {
+        return spine_animation_get_name(wrappee).flatMap { String(cString: $0) }
+    }
+
+    public var duration: Float {
+        return spine_animation_get_duration(wrappee)
+    }
+
+}
+
+@objc(SpineSkeleton)
+@objcMembers
+public final class Skeleton: NSObject {
+
+    internal let wrappee: spine_skeleton
+
+    internal init(_ wrappee: spine_skeleton) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var bounds: Bounds {
+        return .init(spine_skeleton_get_bounds(wrappee))
+    }
+
+    public var rootBone: Bone? {
+        return spine_skeleton_get_root_bone(wrappee).flatMap { .init($0) }
+    }
+
+    public var data: SkeletonData? {
+        return spine_skeleton_get_data(wrappee).flatMap { .init($0) }
+    }
+
+    public var bones: [Bone] {
+        let num = Int(spine_skeleton_get_num_bones(wrappee))
+        let ptr = spine_skeleton_get_bones(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var slots: [Slot] {
+        let num = Int(spine_skeleton_get_num_slots(wrappee))
+        let ptr = spine_skeleton_get_slots(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var drawOrder: [Slot] {
+        let num = Int(spine_skeleton_get_num_draw_order(wrappee))
+        let ptr = spine_skeleton_get_draw_order(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var ikConstraints: [IkConstraint] {
+        let num = Int(spine_skeleton_get_num_ik_constraints(wrappee))
+        let ptr = spine_skeleton_get_ik_constraints(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var transformConstraints: [TransformConstraint] {
+        let num = Int(spine_skeleton_get_num_transform_constraints(wrappee))
+        let ptr = spine_skeleton_get_transform_constraints(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var pathConstraints: [PathConstraint] {
+        let num = Int(spine_skeleton_get_num_path_constraints(wrappee))
+        let ptr = spine_skeleton_get_path_constraints(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var physicsConstraints: [PhysicsConstraint] {
+        let num = Int(spine_skeleton_get_num_physics_constraints(wrappee))
+        let ptr = spine_skeleton_get_physics_constraints(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var color: Color {
+        return .init(spine_skeleton_get_color(wrappee))
+    }
+
+    public var skin: Skin? {
+        get {
+            return spine_skeleton_get_skin(wrappee).flatMap { .init($0) }
+        }
+        set {
+            spine_skeleton_set_skin(wrappee, newValue?.wrappee)
+        }
+    }
+
+    public var x: Float {
+        get {
+            return spine_skeleton_get_x(wrappee)
+        }
+        set {
+            spine_skeleton_set_x(wrappee, newValue)
+        }
+    }
+
+    public var y: Float {
+        get {
+            return spine_skeleton_get_y(wrappee)
+        }
+        set {
+            spine_skeleton_set_y(wrappee, newValue)
+        }
+    }
+
+    public var scaleX: Float {
+        get {
+            return spine_skeleton_get_scale_x(wrappee)
+        }
+        set {
+            spine_skeleton_set_scale_x(wrappee, newValue)
+        }
+    }
+
+    public var scaleY: Float {
+        get {
+            return spine_skeleton_get_scale_y(wrappee)
+        }
+        set {
+            spine_skeleton_set_scale_y(wrappee, newValue)
+        }
+    }
+
+    public var time: Float {
+        get {
+            return spine_skeleton_get_time(wrappee)
+        }
+        set {
+            spine_skeleton_set_time(wrappee, newValue)
+        }
+    }
+
+    public func updateCache() {
+        spine_skeleton_update_cache(wrappee)
+    }
+
+    public func updateWorldTransform(physics: Physics) {
+        spine_skeleton_update_world_transform(wrappee, physics)
+    }
+
+    public func updateWorldTransformBone(physics: Physics, parent: Bone) {
+        spine_skeleton_update_world_transform_bone(wrappee, physics, parent.wrappee)
+    }
+
+    public func setToSetupPose() {
+        spine_skeleton_set_to_setup_pose(wrappee)
+    }
+
+    public func setBonesToSetupPose() {
+        spine_skeleton_set_bones_to_setup_pose(wrappee)
+    }
+
+    public func setSlotsToSetupPose() {
+        spine_skeleton_set_slots_to_setup_pose(wrappee)
+    }
+
+    @discardableResult
+    public func findBone(boneName: String?) -> Bone? {
+        return spine_skeleton_find_bone(wrappee, boneName).flatMap { .init($0) }
+    }
+
+    @discardableResult
+    public func findSlot(slotName: String?) -> Slot? {
+        return spine_skeleton_find_slot(wrappee, slotName).flatMap { .init($0) }
+    }
+
+    @discardableResult
+    public func getAttachmentByName(slotName: String?, attachmentName: String?) -> Attachment? {
+        return spine_skeleton_get_attachment_by_name(wrappee, slotName, attachmentName).flatMap { .init($0) }
+    }
+
+    @discardableResult
+    public func getAttachment(slotIndex: Int32, attachmentName: String?) -> Attachment? {
+        return spine_skeleton_get_attachment(wrappee, slotIndex, attachmentName).flatMap { .init($0) }
+    }
+
+    public func setAttachment(slotName: String?, attachmentName: String?) {
+        spine_skeleton_set_attachment(wrappee, slotName, attachmentName)
+    }
+
+    @discardableResult
+    public func findIkConstraint(constraintName: String?) -> IkConstraint? {
+        return spine_skeleton_find_ik_constraint(wrappee, constraintName).flatMap { .init($0) }
+    }
+
+    @discardableResult
+    public func findTransformConstraint(constraintName: String?) -> TransformConstraint? {
+        return spine_skeleton_find_transform_constraint(wrappee, constraintName).flatMap { .init($0) }
+    }
+
+    @discardableResult
+    public func findPathConstraint(constraintName: String?) -> PathConstraint? {
+        return spine_skeleton_find_path_constraint(wrappee, constraintName).flatMap { .init($0) }
+    }
+
+    @discardableResult
+    public func findPhysicsConstraint(constraintName: String?) -> PhysicsConstraint? {
+        return spine_skeleton_find_physics_constraint(wrappee, constraintName).flatMap { .init($0) }
+    }
+
+    public func setColor(r: Float, g: Float, b: Float, a: Float) {
+        spine_skeleton_set_color(wrappee, r, g, b, a)
+    }
+
+    public func setPosition(x: Float, y: Float) {
+        spine_skeleton_set_position(wrappee, x, y)
+    }
+
+    public func update(delta: Float) {
+        spine_skeleton_update(wrappee, delta)
+    }
+
+    public func setSkinByName(skinName: String?) {
+        spine_skeleton_set_skin_by_name(wrappee, skinName)
+    }
+
+}
+
+@objc(SpineSequence)
+@objcMembers
+public final class Sequence: NSObject {
+
+    internal let wrappee: spine_sequence
+
+    internal init(_ wrappee: spine_sequence) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var regions: [TextureRegion] {
+        let num = Int(spine_sequence_get_num_regions(wrappee))
+        let ptr = spine_sequence_get_regions(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var id: Int32 {
+        get {
+            return spine_sequence_get_id(wrappee)
+        }
+        set {
+            spine_sequence_set_id(wrappee, newValue)
+        }
+    }
+
+    public var start: Int32 {
+        get {
+            return spine_sequence_get_start(wrappee)
+        }
+        set {
+            spine_sequence_set_start(wrappee, newValue)
+        }
+    }
+
+    public var digits: Int32 {
+        get {
+            return spine_sequence_get_digits(wrappee)
+        }
+        set {
+            spine_sequence_set_digits(wrappee, newValue)
+        }
+    }
+
+    public var setupIndex: Int32 {
+        get {
+            return spine_sequence_get_setup_index(wrappee)
+        }
+        set {
+            spine_sequence_set_setup_index(wrappee, newValue)
+        }
+    }
+
+    public func apply(slot: Slot, attachment: Attachment) {
+        spine_sequence_apply(wrappee, slot.wrappee, attachment.wrappee)
+    }
+
+    @discardableResult
+    public func getPath(basePath: String?, index: Int32) -> String? {
+        return spine_sequence_get_path(wrappee, basePath, index).flatMap { String(cString: $0) }
+    }
+
+}
+
+@objc(SpineBounds)
+@objcMembers
+public final class Bounds: NSObject {
+
+    internal let wrappee: spine_bounds
+
+    internal init(_ wrappee: spine_bounds) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var x: Float {
+        return spine_bounds_get_x(wrappee)
+    }
+
+    public var y: Float {
+        return spine_bounds_get_y(wrappee)
+    }
+
+    public var width: Float {
+        return spine_bounds_get_width(wrappee)
+    }
+
+    public var height: Float {
+        return spine_bounds_get_height(wrappee)
+    }
+
+}
+
+@objc(SpineVector)
+@objcMembers
+public final class Vector: NSObject {
+
+    internal let wrappee: spine_vector
+
+    internal init(_ wrappee: spine_vector) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var x: Float {
+        return spine_vector_get_x(wrappee)
+    }
+
+    public var y: Float {
+        return spine_vector_get_y(wrappee)
+    }
+
+}
+
+@objc(SpineEvent)
+@objcMembers
+public final class Event: NSObject {
+
+    internal let wrappee: spine_event
+
+    internal init(_ wrappee: spine_event) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var data: EventData {
+        return .init(spine_event_get_data(wrappee))
+    }
+
+    public var time: Float {
+        return spine_event_get_time(wrappee)
+    }
+
+    public var intValue: Int32 {
+        get {
+            return spine_event_get_int_value(wrappee)
+        }
+        set {
+            spine_event_set_int_value(wrappee, newValue)
+        }
+    }
+
+    public var floatValue: Float {
+        get {
+            return spine_event_get_float_value(wrappee)
+        }
+        set {
+            spine_event_set_float_value(wrappee, newValue)
+        }
+    }
+
+    public var stringValue: String? {
+        get {
+            return spine_event_get_string_value(wrappee).flatMap { String(cString: $0) }
+        }
+        set {
+            spine_event_set_string_value(wrappee, newValue)
+        }
+    }
+
+    public var volume: Float {
+        get {
+            return spine_event_get_volume(wrappee)
+        }
+        set {
+            spine_event_set_volume(wrappee, newValue)
+        }
+    }
+
+    public var balance: Float {
+        get {
+            return spine_event_get_balance(wrappee)
+        }
+        set {
+            spine_event_set_balance(wrappee, newValue)
+        }
+    }
+
+}
+
+@objc(SpineAtlas)
+@objcMembers
+public final class Atlas: NSObject {
+
+    internal let wrappee: spine_atlas
+    internal var disposed = false
+
+    internal init(_ wrappee: spine_atlas) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var isPma: Bool {
+        return spine_atlas_is_pma(wrappee) != 0
+    }
+
+    public var error: String? {
+        return spine_atlas_get_error(wrappee).flatMap { String(cString: $0) }
+    }
+
+    @discardableResult
+    public func load(atlasData: String?) -> Atlas {
+        return .init(spine_atlas_load(atlasData))
+    }
+
+    @discardableResult
+    public func getImagePath(index: Int32) -> String? {
+        return spine_atlas_get_image_path(wrappee, index).flatMap { String(cString: $0) }
+    }
+
+    public func dispose() {
+        if disposed { return }
+        disposed = true
+        spine_atlas_dispose(wrappee)
+    }
+
+}
+
+@objc(SpineColor)
+@objcMembers
+public final class Color: NSObject {
+
+    internal let wrappee: spine_color
+
+    internal init(_ wrappee: spine_color) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var r: Float {
+        return spine_color_get_r(wrappee)
+    }
+
+    public var g: Float {
+        return spine_color_get_g(wrappee)
+    }
+
+    public var b: Float {
+        return spine_color_get_b(wrappee)
+    }
+
+    public var a: Float {
+        return spine_color_get_a(wrappee)
+    }
+
+}
+
+@objc(SpineBone)
+@objcMembers
+public final class Bone: NSObject {
+
+    internal let wrappee: spine_bone
+
+    internal init(_ wrappee: spine_bone) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public func setIsYDown(yDown: Bool) {
+        spine_bone_set_is_y_down(yDown ? -1 : 0)
+    }
+
+    public var worldToLocalRotationX: Float {
+        return spine_bone_get_world_to_local_rotation_x(wrappee)
+    }
+
+    public var worldToLocalRotationY: Float {
+        return spine_bone_get_world_to_local_rotation_y(wrappee)
+    }
+
+    public var data: BoneData {
+        return .init(spine_bone_get_data(wrappee))
+    }
+
+    public var skeleton: Skeleton {
+        return .init(spine_bone_get_skeleton(wrappee))
+    }
+
+    public var parent: Bone? {
+        return spine_bone_get_parent(wrappee).flatMap { .init($0) }
+    }
+
+    public var children: [Bone] {
+        let num = Int(spine_bone_get_num_children(wrappee))
+        let ptr = spine_bone_get_children(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var worldRotationX: Float {
+        return spine_bone_get_world_rotation_x(wrappee)
+    }
+
+    public var worldRotationY: Float {
+        return spine_bone_get_world_rotation_y(wrappee)
+    }
+
+    public var worldScaleX: Float {
+        return spine_bone_get_world_scale_x(wrappee)
+    }
+
+    public var worldScaleY: Float {
+        return spine_bone_get_world_scale_y(wrappee)
+    }
+
+    public var x: Float {
+        get {
+            return spine_bone_get_x(wrappee)
+        }
+        set {
+            spine_bone_set_x(wrappee, newValue)
+        }
+    }
+
+    public var y: Float {
+        get {
+            return spine_bone_get_y(wrappee)
+        }
+        set {
+            spine_bone_set_y(wrappee, newValue)
+        }
+    }
+
+    public var rotation: Float {
+        get {
+            return spine_bone_get_rotation(wrappee)
+        }
+        set {
+            spine_bone_set_rotation(wrappee, newValue)
+        }
+    }
+
+    public var scaleX: Float {
+        get {
+            return spine_bone_get_scale_x(wrappee)
+        }
+        set {
+            spine_bone_set_scale_x(wrappee, newValue)
+        }
+    }
+
+    public var scaleY: Float {
+        get {
+            return spine_bone_get_scale_y(wrappee)
+        }
+        set {
+            spine_bone_set_scale_y(wrappee, newValue)
+        }
+    }
+
+    public var shearX: Float {
+        get {
+            return spine_bone_get_shear_x(wrappee)
+        }
+        set {
+            spine_bone_set_shear_x(wrappee, newValue)
+        }
+    }
+
+    public var shearY: Float {
+        get {
+            return spine_bone_get_shear_y(wrappee)
+        }
+        set {
+            spine_bone_set_shear_y(wrappee, newValue)
+        }
+    }
+
+    public var appliedRotation: Float {
+        get {
+            return spine_bone_get_applied_rotation(wrappee)
+        }
+        set {
+            spine_bone_set_applied_rotation(wrappee, newValue)
+        }
+    }
+
+    public var aX: Float {
+        get {
+            return spine_bone_get_a_x(wrappee)
+        }
+        set {
+            spine_bone_set_a_x(wrappee, newValue)
+        }
+    }
+
+    public var aY: Float {
+        get {
+            return spine_bone_get_a_y(wrappee)
+        }
+        set {
+            spine_bone_set_a_y(wrappee, newValue)
+        }
+    }
+
+    public var aScaleX: Float {
+        get {
+            return spine_bone_get_a_scale_x(wrappee)
+        }
+        set {
+            spine_bone_set_a_scale_x(wrappee, newValue)
+        }
+    }
+
+    public var aScaleY: Float {
+        get {
+            return spine_bone_get_a_scale_y(wrappee)
+        }
+        set {
+            spine_bone_set_a_scale_y(wrappee, newValue)
+        }
+    }
+
+    public var aShearX: Float {
+        get {
+            return spine_bone_get_a_shear_x(wrappee)
+        }
+        set {
+            spine_bone_set_a_shear_x(wrappee, newValue)
+        }
+    }
+
+    public var aShearY: Float {
+        get {
+            return spine_bone_get_a_shear_y(wrappee)
+        }
+        set {
+            spine_bone_set_a_shear_y(wrappee, newValue)
+        }
+    }
+
+    public var a: Float {
+        get {
+            return spine_bone_get_a(wrappee)
+        }
+        set {
+            spine_bone_set_a(wrappee, newValue)
+        }
+    }
+
+    public var b: Float {
+        get {
+            return spine_bone_get_b(wrappee)
+        }
+        set {
+            spine_bone_set_b(wrappee, newValue)
+        }
+    }
+
+    public var c: Float {
+        get {
+            return spine_bone_get_c(wrappee)
+        }
+        set {
+            spine_bone_set_c(wrappee, newValue)
+        }
+    }
+
+    public var d: Float {
+        get {
+            return spine_bone_get_d(wrappee)
+        }
+        set {
+            spine_bone_set_d(wrappee, newValue)
+        }
+    }
+
+    public var worldX: Float {
+        get {
+            return spine_bone_get_world_x(wrappee)
+        }
+        set {
+            spine_bone_set_world_x(wrappee, newValue)
+        }
+    }
+
+    public var worldY: Float {
+        get {
+            return spine_bone_get_world_y(wrappee)
+        }
+        set {
+            spine_bone_set_world_y(wrappee, newValue)
+        }
+    }
+
+    public var isActive: Bool {
+        get {
+            return spine_bone_get_is_active(wrappee) != 0
+        }
+        set {
+            spine_bone_set_is_active(wrappee, newValue ? -1 : 0)
+        }
+    }
+
+    public var inherit: Inherit {
+        get {
+            return spine_bone_get_inherit(wrappee)
+        }
+        set {
+            spine_bone_set_inherit(wrappee, newValue)
+        }
+    }
+
+    public var isYDown: Bool {
+        return spine_bone_get_is_y_down() != 0
+    }
+
+    public func update() {
+        spine_bone_update(wrappee)
+    }
+
+    public func updateWorldTransform() {
+        spine_bone_update_world_transform(wrappee)
+    }
+
+    public func updateWorldTransformWith(x: Float, y: Float, rotation: Float, scaleX: Float, scaleY: Float, shearX: Float, shearY: Float) {
+        spine_bone_update_world_transform_with(wrappee, x, y, rotation, scaleX, scaleY, shearX, shearY)
+    }
+
+    public func updateAppliedTransform() {
+        spine_bone_update_applied_transform(wrappee)
+    }
+
+    public func setToSetupPose() {
+        spine_bone_set_to_setup_pose(wrappee)
+    }
+
+    @discardableResult
+    public func worldToLocal(worldX: Float, worldY: Float) -> Vector {
+        return .init(spine_bone_world_to_local(wrappee, worldX, worldY))
+    }
+
+    @discardableResult
+    public func worldToParent(worldX: Float, worldY: Float) -> Vector {
+        return .init(spine_bone_world_to_parent(wrappee, worldX, worldY))
+    }
+
+    @discardableResult
+    public func localToWorld(localX: Float, localY: Float) -> Vector {
+        return .init(spine_bone_local_to_world(wrappee, localX, localY))
+    }
+
+    @discardableResult
+    public func parentToWorld(localX: Float, localY: Float) -> Vector {
+        return .init(spine_bone_parent_to_world(wrappee, localX, localY))
+    }
+
+    @discardableResult
+    public func worldToLocalRotation(worldRotation: Float) -> Float {
+        return spine_bone_world_to_local_rotation(wrappee, worldRotation)
+    }
+
+    @discardableResult
+    public func localToWorldRotation(localRotation: Float) -> Float {
+        return spine_bone_local_to_world_rotation(wrappee, localRotation)
+    }
+
+    public func rotateWorld(degrees: Float) {
+        spine_bone_rotate_world(wrappee, degrees)
+    }
+
+}
+
+@objc(SpineSlot)
+@objcMembers
+public final class Slot: NSObject {
+
+    internal let wrappee: spine_slot
+
+    internal init(_ wrappee: spine_slot) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var data: SlotData {
+        return .init(spine_slot_get_data(wrappee))
+    }
+
+    public var bone: Bone {
+        return .init(spine_slot_get_bone(wrappee))
+    }
+
+    public var skeleton: Skeleton {
+        return .init(spine_slot_get_skeleton(wrappee))
+    }
+
+    public var color: Color {
+        return .init(spine_slot_get_color(wrappee))
+    }
+
+    public var darkColor: Color {
+        return .init(spine_slot_get_dark_color(wrappee))
+    }
+
+    public var attachment: Attachment? {
+        get {
+            return spine_slot_get_attachment(wrappee).flatMap { .init($0) }
+        }
+        set {
+            spine_slot_set_attachment(wrappee, newValue?.wrappee)
+        }
+    }
+
+    public var sequenceIndex: Int32 {
+        get {
+            return spine_slot_get_sequence_index(wrappee)
+        }
+        set {
+            spine_slot_set_sequence_index(wrappee, newValue)
+        }
+    }
+
+    public func setToSetupPose() {
+        spine_slot_set_to_setup_pose(wrappee)
+    }
+
+    public func setColor(r: Float, g: Float, b: Float, a: Float) {
+        spine_slot_set_color(wrappee, r, g, b, a)
+    }
+
+    public func setDarkColor(r: Float, g: Float, b: Float, a: Float) {
+        spine_slot_set_dark_color(wrappee, r, g, b, a)
+    }
+
+    @discardableResult
+    public func hasDarkColor() -> Bool {
+        return spine_slot_has_dark_color(wrappee) != 0
+    }
+
+}
+
+@objc(SpineSkin)
+@objcMembers
+public final class Skin: NSObject {
+
+    internal let wrappee: spine_skin
+    internal var disposed = false
+
+    internal init(_ wrappee: spine_skin) {
+        self.wrappee = wrappee
+        super.init()
+    }
+
+    public var name: String? {
+        return spine_skin_get_name(wrappee).flatMap { String(cString: $0) }
+    }
+
+    public var entries: SkinEntries {
+        return .init(spine_skin_get_entries(wrappee))
+    }
+
+    public var bones: [BoneData] {
+        let num = Int(spine_skin_get_num_bones(wrappee))
+        let ptr = spine_skin_get_bones(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public var constraints: [ConstraintData] {
+        let num = Int(spine_skin_get_num_constraints(wrappee))
+        let ptr = spine_skin_get_constraints(wrappee)
+        return (0..<num).compactMap {
+            ptr?[$0].flatMap { .init($0) }
+        }
+    }
+
+    public func setAttachment(slotIndex: Int32, name: String?, attachment: Attachment) {
+        spine_skin_set_attachment(wrappee, slotIndex, name, attachment.wrappee)
+    }
+
+    @discardableResult
+    public func getAttachment(slotIndex: Int32, name: String?) -> Attachment? {
+        return spine_skin_get_attachment(wrappee, slotIndex, name).flatMap { .init($0) }
+    }
+
+    public func removeAttachment(slotIndex: Int32, name: String?) {
+        spine_skin_remove_attachment(wrappee, slotIndex, name)
+    }
+
+    public func addSkin(other: Skin) {
+        spine_skin_add_skin(wrappee, other.wrappee)
+    }
+
+    public func copySkin(other: Skin) {
+        spine_skin_copy_skin(wrappee, other.wrappee)
+    }
+
+    public func dispose() {
+        if disposed { return }
+        disposed = true
+        spine_skin_dispose(wrappee)
+    }
+
+}
+

+ 215 - 0
spine-ios/Sources/Spine/SpineController.swift

@@ -0,0 +1,215 @@
+import Foundation
+import CoreGraphics
+import QuartzCore
+import UIKit
+
+public typealias SpineControllerCallback = (_ controller: SpineController) -> Void
+
+/// Controls how the skeleton of a ``SpineUIView`` is animated and rendered.
+///
+/// Upon initialization of a ``SpineUIView`` the provided `onInitialized` callback method is called once. This method can be used
+/// to setup the initial animation(s) of the skeleton, among other things.
+///
+/// After initialization is complete, the ``SpineUIView`` is rendered at the screen refresh rate. In each frame,
+/// the ``AnimationState`` is updated and applied to the ``Skeleton``.
+///
+/// Next the optionally provided method `onBeforeUpdateWorldTransforms` is called, which can modify the
+/// skeleton before its current pose is calculated using ``Skeleton/updateWorldTransform(physics:)``. After
+/// ``Skeleton.updateWorldTransforms`` has completed, the optional `onAfterUpdateWorldTransforms` method is
+/// called, which can modify the current pose before rendering the skeleton.
+///
+/// Before the skeleton's current pose is rendered by the ``SpineUIView`` the optional `onBeforePaint` is called,
+/// which allows rendering backgrounds or other objects that should go behind the skeleton in your view hierarchy. The
+/// ``SpineUIView`` then renderes the skeleton's current pose, and finally calls the optional `onAfterPaint`, after which you
+/// can render additional objects on top of the skeleton in your view hierarchy.
+///
+/// The underlying ``Atlas``, ``SkeletonData``, ``Skeleton``, ``AnimationStateData``, ``AnimationState``, and ``SkeletonDrawable``
+/// can be accessed through their respective getters to inspect and/or modify the skeleton and its associated data. Accessing
+/// this data is only allowed if the ``SpineUIView`` and its data have been initialized and have not been disposed yet.
+///
+/// By default, the view updates and renders the skeleton every frame. The `pause` method can be used to pause updating
+/// and rendering the skeleton. The `resume` method resumes updating and rendering the skeleton. The `isPlaying` property
+/// reports the current state.
+///
+/// Per default, ``SkeletonDrawableWrapper`` is disposed when ``SpineController`` is deinitialized. You can disable this behaviour with the ``disposeDrawableOnDeInit`` contructor parameter.
+@objcMembers
+public final class SpineController: NSObject, ObservableObject {
+    
+    public internal(set) var drawable: SkeletonDrawableWrapper!
+    
+    private let onInitialized: SpineControllerCallback?
+    private let onBeforeUpdateWorldTransforms: SpineControllerCallback?
+    private let onAfterUpdateWorldTransforms: SpineControllerCallback?
+    private let onBeforePaint: SpineControllerCallback?
+    private let onAfterPaint: SpineControllerCallback?
+    private let disposeDrawableOnDeInit: Bool
+    
+    private var scaleX: CGFloat = 1
+    private var scaleY: CGFloat = 1
+    private var offsetX: CGFloat = 0
+    private var offsetY: CGFloat = 0
+    
+    @Published
+    public private(set) var isPlaying: Bool = true
+    
+    @Published
+    public private(set) var viewSize: CGSize = .zero
+    
+    /// Constructs a new ``SpineUIview`` controller. See the class documentation of ``SpineWidgetController`` for information on
+    /// the optional arguments.
+    public init(
+        onInitialized: SpineControllerCallback? = nil,
+        onBeforeUpdateWorldTransforms: SpineControllerCallback? = nil,
+        onAfterUpdateWorldTransforms: SpineControllerCallback? = nil,
+        onBeforePaint: SpineControllerCallback? = nil,
+        onAfterPaint: SpineControllerCallback? = nil,
+        disposeDrawableOnDeInit: Bool = true
+    ) {
+        self.onInitialized = onInitialized
+        self.onBeforeUpdateWorldTransforms = onBeforeUpdateWorldTransforms
+        self.onAfterUpdateWorldTransforms = onAfterUpdateWorldTransforms
+        self.onBeforePaint = onBeforePaint
+        self.onAfterPaint = onAfterPaint
+        self.disposeDrawableOnDeInit = disposeDrawableOnDeInit
+        
+        super.init()
+    }
+    
+    deinit {
+        if disposeDrawableOnDeInit {
+            drawable?.dispose() // TODO move drawable out of view?
+        }
+    }
+    
+    /// The ``Atlas`` from which images to render the skeleton are sourced.
+    public var atlas: Atlas {
+        drawable.atlas
+    }
+    
+    /// The setup-pose data used by the skeleton.
+    public var skeletonData: SkeletonData {
+        drawable.skeletonData
+    }
+    
+    /// The ``Skeleton``
+    public var skeleton: Skeleton {
+        drawable.skeleton
+    }
+    
+    /// The mixing information used by the ``AnimationState``
+    public var animationStateData: AnimationStateData {
+        drawable.animationStateData
+    }
+    
+    /// The ``AnimationState`` used to manage animations that are being applied to the
+    /// skeleton.
+    public var animationState: AnimationState {
+        drawable.animationState
+    }
+    
+    /// The ``AnimationStateWrapper`` used to hold ``AnimationState``, register ``AnimationStateListener`` and call ``AnimationStateWrapper/update(delta:)``
+    public var animationStateWrapper: AnimationStateWrapper {
+        drawable.animationStateWrapper
+    }
+    
+    /// Transforms the coordinates given in the ``SpineUIView`` coordinate system in `position` to
+    /// the skeleton coordinate system. See the `IKFollowing.swift` example how to use this
+    /// to move a bone based on user touch input.
+    public func toSkeletonCoordinates(position: CGPoint) -> CGPoint {
+        let x = position.x;
+        let y = position.y;
+        return CGPoint(
+            x: (x - viewSize.width / 2) / scaleX - offsetX,
+            y: (y - viewSize.height / 2) / scaleY - offsetY
+        )
+    }
+    
+    /// Transforms the coordinates given in skeleton coordinate system to
+    /// the the ``SpineUIView`` coordinates. See the `DebugRendering.swift` example hot to use this to draw rectangles over skeleton bones for debugging purposes.
+    public func fromSkeletonCoordinates(position: CGPoint) -> CGPoint {
+        let x = position.x;
+        let y = position.y;
+        return CGPoint(
+            x: (x + offsetX) * scaleX,
+            y: (y + offsetY) * scaleY
+        )
+    }
+    
+    /// Pauses updating and rendering the skeleton.
+    public func pause() {
+        isPlaying = false
+    }
+    
+    /// Resumes updating and rendering the skeleton.
+    public func resume() {
+        isPlaying = true
+    }
+    
+    internal func load(atlasFile: String, skeletonFile: String, bundle: Bundle = .main) async throws {
+        let atlasAndPages = try await Atlas.fromBundle(atlasFile, bundle: bundle)
+        let skeletonData = try await SkeletonData.fromBundle(
+            atlas: atlasAndPages.0,
+            skeletonFileName: skeletonFile,
+            bundle: bundle
+        )
+        try await MainActor.run {
+            let skeletonDrawableWrapper = try SkeletonDrawableWrapper(
+                atlas: atlasAndPages.0,
+                atlasPages: atlasAndPages.1,
+                skeletonData: skeletonData
+            )
+            self.drawable = skeletonDrawableWrapper
+        }
+    }
+    
+    internal func initialize() {
+        onInitialized?(self)
+    }
+    
+}
+
+extension SpineController: SpineRendererDelegate {
+    
+    func spineRendererWillDraw(_ spineRenderer: SpineRenderer) {
+        onBeforePaint?(self)
+    }
+    
+    func spineRendererDidDraw(_ spineRenderer: SpineRenderer) {
+        onAfterPaint?(self)
+    }
+    
+    func spineRendererDidUpdate(_ spineRenderer: SpineRenderer, scaleX: CGFloat, scaleY: CGFloat, offsetX: CGFloat, offsetY: CGFloat, size: CGSize) {
+        self.scaleX = scaleX
+        self.scaleY = scaleY
+        self.offsetX = offsetX
+        self.offsetY = offsetY
+        self.viewSize = size
+    }
+}
+
+extension SpineController: SpineRendererDataSource {
+    
+    func spineRendererWillUpdate(_ spineRenderer: SpineRenderer) {
+        onBeforeUpdateWorldTransforms?(self)
+    }
+    
+    func spineRendererDidUpdate(_ spineRenderer: SpineRenderer) {
+        onAfterUpdateWorldTransforms?(self)
+    }
+    
+    func spineRenderer(_ spineRenderer: SpineRenderer, needsUpdate delta: TimeInterval) {
+        drawable?.update(delta: Float(delta))
+    }
+    
+    func isPlaying(_ spineRenderer: SpineRenderer) -> Bool {
+        return isPlaying
+    }
+    
+    func skeletonDrawable(_ spineRenderer: SpineRenderer) -> SkeletonDrawableWrapper {
+        return drawable
+    }
+    
+    func renderCommands(_ spineRenderer: SpineRenderer) -> [RenderCommand] {
+        return drawable?.skeletonDrawable.render() ?? []
+    }
+}

+ 280 - 0
spine-ios/Sources/Spine/SpineUIView.swift

@@ -0,0 +1,280 @@
+import UIKit
+import MetalKit
+
+/// A ``UIView`` to display a Spine skeleton. The skeleton can be loaded from a bundle, local files, http, or a pre-loaded ``SkeletonDrawableWrapper``.
+///
+/// The skeleton displayed by a ``SpineUIView`` can be controlled via a ``SpineController``.
+///
+/// The size of the widget can be derived from the bounds provided by a ``BoundsProvider``. If the view is not sized by the bounds
+/// computed by the ``BoundsProvider``, the widget will use the computed bounds to fit the skeleton inside the view's dimensions.
+///
+/// This is a direct subclass of ``MTKView`` and is using `Metal` to render the skeleton.
+@objc
+public final class SpineUIView: MTKView {
+    
+    let controller: SpineController
+    let mode: Spine.ContentMode
+    let alignment: Spine.Alignment
+    let boundsProvider: BoundsProvider
+    
+    internal var computedBounds: CGRect = .zero
+    internal var renderer: SpineRenderer?
+    
+    @objc internal init(
+        controller: SpineController = SpineController(),
+        mode: Spine.ContentMode = .fit,
+        alignment: Spine.Alignment = .center,
+        boundsProvider: BoundsProvider = SetupPoseBounds(),
+        backgroundColor: UIColor = .clear
+    ) {
+        self.controller = controller
+        self.mode = mode
+        self.alignment = alignment
+        self.boundsProvider = boundsProvider
+        
+        super.init(frame: .zero, device: SpineObjects.shared.device)
+        clearColor = MTLClearColor(backgroundColor)
+        isOpaque = backgroundColor != .clear
+    }
+    
+    /// An initializer that constructs a new ``SpineUIView`` from a ``SpineViewSource``.
+    ///
+    /// After initialization is complete, the provided `controller` is invoked as per the ``SpineController`` semantics, to allow
+    /// modifying how the skeleton inside the widget is animated and rendered.
+    ///
+    /// - Parameters:
+    ///     - from: Specifies the ``SpineViewSource`` from which to load `atlas` and `skeleton` data.
+    ///     - controller: The ``SpineController`` used to modify how the skeleton inside the view is animated and rendered.
+    ///     - mode: How the skeleton is fitted inside ``SpineUIView``. Per default, it is `.fit`
+    ///     - alignment: How the skeleton is alignment inside ``SpineUIView``. Per default, it is `.center`
+    ///     - boundsProvider: The skeleton bounds must be computed via a ``BoundsProvider``. Per default, ``SetupPoseBounds`` is used.
+    ///     - backgroundColor: The background color of the view. Per defaut, `UIColor.clear` is used
+    ///
+    /// - Returns: A new instance of ``SpineUIView``.
+    public convenience init(
+        from source: SpineViewSource,
+        controller: SpineController = SpineController(),
+        mode: Spine.ContentMode = .fit,
+        alignment: Spine.Alignment = .center,
+        boundsProvider: BoundsProvider = SetupPoseBounds(),
+        backgroundColor: UIColor = .clear
+    ) {
+        self.init(controller: controller, mode: mode, alignment: alignment, boundsProvider: boundsProvider, backgroundColor: backgroundColor)
+        Task.detached(priority: .high) {
+            do {
+                let drawable = try await source.loadDrawable()
+                try await self.load(drawable: drawable)
+            } catch {
+                print(error)
+            }
+        }
+    }
+    
+    /// A convenience initializer that constructs a new ``SpineUIView`` from bundled files.
+    ///
+    /// After initialization is complete, the provided `controller` is invoked as per the ``SpineController`` semantics, to allow
+    /// modifying how the skeleton inside the widget is animated and rendered.
+    ///
+    /// - Parameters:
+    ///     - atlasFileName: Specifies the `.atlas` file to be loaded for the images used to render the skeleton
+    ///     - skeletonFileName: Specifies either a Skeleton `.json` or `.skel` file containing the skeleton data
+    ///     - bundle: Specifies from which bundle to load the files. Per default, it is `Bundle.main`
+    ///     - controller: The ``SpineController`` used to modify how the skeleton inside the view is animated and rendered.
+    ///     - mode: How the skeleton is fitted inside ``SpineUIView``. Per default, it is `.fit`
+    ///     - alignment: How the skeleton is alignment inside ``SpineUIView``. Per default, it is `.center`
+    ///     - boundsProvider: The skeleton bounds must be computed via a ``BoundsProvider``. Per default, ``SetupPoseBounds`` is used.
+    ///     - backgroundColor: The background color of the view. Per defaut, `UIColor.clear` is used
+    ///
+    /// - Returns: A new instance of ``SpineUIView``.
+    @objc public convenience init(
+        atlasFileName: String,
+        skeletonFileName: String,
+        bundle: Bundle = .main,
+        controller: SpineController = SpineController(),
+        mode: Spine.ContentMode = .fit,
+        alignment: Spine.Alignment = .center,
+        boundsProvider: BoundsProvider = SetupPoseBounds(),
+        backgroundColor: UIColor = .clear
+    ) {
+        self.init(from: .bundle(atlasFileName: atlasFileName, skeletonFileName: skeletonFileName, bundle: bundle), controller: controller, mode: mode, alignment: alignment, boundsProvider: boundsProvider, backgroundColor: backgroundColor)
+    }
+    
+    /// A convenience initializer that constructs a new ``SpineUIView`` from file URLs.
+    ///
+    /// After initialization is complete, the provided `controller` is invoked as per the ``SpineController`` semantics, to allow
+    /// modifying how the skeleton inside the widget is animated and rendered.
+    ///
+    /// - Parameters:
+    ///     - atlasFile: Specifies the `.atlas` file to be loaded for the images used to render the skeleton
+    ///     - skeletonFile: Specifies either a Skeleton `.json` or `.skel` file containing the skeleton data
+    ///     - controller: The ``SpineController`` used to modify how the skeleton inside the view is animated and rendered.
+    ///     - mode: How the skeleton is fitted inside ``SpineUIView``. Per default, it is `.fit`
+    ///     - alignment: How the skeleton is alignment inside ``SpineUIView``. Per default, it is `.center`
+    ///     - boundsProvider: The skeleton bounds must be computed via a ``BoundsProvider``. Per default, ``SetupPoseBounds`` is used.
+    ///     - backgroundColor: The background color of the view. Per defaut, `UIColor.clear` is used
+    ///
+    /// - Returns: A new instance of ``SpineUIView``.
+    @objc public convenience init(
+        atlasFile: URL,
+        skeletonFile: URL,
+        controller: SpineController = SpineController(),
+        mode: Spine.ContentMode = .fit,
+        alignment: Spine.Alignment = .center,
+        boundsProvider: BoundsProvider = SetupPoseBounds(),
+        backgroundColor: UIColor = .clear
+    ) {
+        self.init(from: .file(atlasFile: atlasFile, skeletonFile: skeletonFile), controller: controller, mode: mode, alignment: alignment, boundsProvider: boundsProvider, backgroundColor: backgroundColor)
+    }
+    
+    /// A convenience initializer that constructs a new ``SpineUIView`` from HTTP.
+    ///
+    /// After initialization is complete, the provided `controller` is invoked as per the ``SpineController`` semantics, to allow
+    /// modifying how the skeleton inside the widget is animated and rendered.
+    ///
+    /// - Parameters:
+    ///     - atlasURL: Specifies the `.atlas` file http URL to be loaded for the images used to render the skeleton
+    ///     - skeletonURL: Specifies either a Skeleton `.json` or `.skel` file http URL containing the skeleton data
+    ///     - controller: The ``SpineController`` used to modify how the skeleton inside the view is animated and rendered.
+    ///     - mode: How the skeleton is fitted inside ``SpineUIView``. Per default, it is `.fit`
+    ///     - alignment: How the skeleton is alignment inside ``SpineUIView``. Per default, it is `.center`
+    ///     - boundsProvider: The skeleton bounds must be computed via a ``BoundsProvider``. Per default, ``SetupPoseBounds`` is used.
+    ///     - backgroundColor: The background color of the view. Per defaut, `UIColor.clear` is used
+    ///
+    /// - Returns: A new instance of ``SpineUIView``.
+    @objc public convenience init(
+        atlasURL: URL,
+        skeletonURL: URL,
+        controller: SpineController = SpineController(),
+        mode: Spine.ContentMode = .fit,
+        alignment: Spine.Alignment = .center,
+        boundsProvider: BoundsProvider = SetupPoseBounds(),
+        backgroundColor: UIColor = .clear
+    ) {
+        self.init(from: .http(atlasURL: atlasURL, skeletonURL: skeletonURL), controller: controller, mode: mode, alignment: alignment, boundsProvider: boundsProvider, backgroundColor: backgroundColor)
+    }
+    
+    /// A convenience initializer that constructs a new ``SpineUIView`` with a ``SkeletonDrawableWrapper``.
+    ///
+    /// After initialization is complete, the provided `controller` is invoked as per the ``SpineController`` semantics, to allow
+    /// modifying how the skeleton inside the widget is animated and rendered.
+    ///
+    /// - Parameters:
+    ///     - drawable: The ``SkeletonDrawableWrapper`` provided directly to the ``SpineController``
+    ///     - controller: The ``SpineController`` used to modify how the skeleton inside the view is animated and rendered.
+    ///     - mode: How the skeleton is fitted inside ``SpineUIView``. Per default, it is `.fit`
+    ///     - alignment: How the skeleton is alignment inside ``SpineUIView``. Per default, it is `.center`
+    ///     - boundsProvider: The skeleton bounds must be computed via a ``BoundsProvider``. Per default, ``SetupPoseBounds`` is used.
+    ///
+    /// - Returns: A new instance of ``SpineUIView``.
+    @objc public convenience init(
+        drawable: SkeletonDrawableWrapper,
+        controller: SpineController = SpineController(),
+        mode: Spine.ContentMode = .fit,
+        alignment: Spine.Alignment = .center,
+        boundsProvider: BoundsProvider = SetupPoseBounds(),
+        backgroundColor: UIColor = .clear
+    ) {
+        self.init(from: .drawable(drawable), controller: controller, mode: mode, alignment: alignment, boundsProvider: boundsProvider, backgroundColor: backgroundColor)
+    }
+    
+    internal override init(frame frameRect: CGRect, device: MTLDevice?) {
+        fatalError("init(frame: device:) has not been implemented. Use init() instead.")
+    }
+    
+    internal required init(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented. Use init() instead.")
+    }
+    
+    /// Disable or enable rendering. Disable it when the spine view is out of bounds and you want to preserve CPU/GPU resources.
+    public var isRendering: Bool {
+        get { !super.isPaused }
+        set {
+            super.isPaused = !newValue
+            if !isPaused {
+                renderer?.lastDraw = CACurrentMediaTime()
+            }
+        }
+    }
+}
+
+extension SpineUIView {
+    
+    internal func load(drawable: SkeletonDrawableWrapper) throws {
+        controller.drawable = drawable
+        computedBounds = boundsProvider.computeBounds(for: drawable)
+        try initRenderer(
+            atlasPages: controller.drawable.atlasPages
+        )
+        controller.initialize()
+    }
+    
+    private func initRenderer(atlasPages: [UIImage]) throws {
+        renderer = try SpineRenderer(
+            device: SpineObjects.shared.device,
+            commandQueue: SpineObjects.shared.commandQueue,
+            pixelFormat: colorPixelFormat,
+            atlasPages: atlasPages,
+            pma: controller.drawable.atlas.isPma
+        )
+        renderer?.delegate = controller
+        renderer?.dataSource = controller
+        renderer?.mtkView(self, drawableSizeWillChange: drawableSize)
+        delegate = renderer
+    }
+}
+
+/// Defines from which source the ``SkeletonDrawableWrapper`` holding `atlas` and `skeleton` data is loaded.
+///
+/// The following sources are supported:
+///     - bundle: Provide file names of your `atlas` and `skeleton` files, including the file extension, to load them from a ``Bundle``. Per defailt, ``Bundle.main`` is used.
+///     - file: Provide file URLs to the `atlas` and `skeleton` files.
+///     - http: Provide http URLs to the `atlas` and `skeleton` files.
+///     - drawable: Directly provide a ``SkeletonDrawableWrapper``
+///
+public enum SpineViewSource {
+    case bundle(atlasFileName: String, skeletonFileName: String, bundle: Bundle = .main)
+    case file(atlasFile: URL, skeletonFile: URL)
+    case http(atlasURL: URL, skeletonURL: URL)
+    case drawable(SkeletonDrawableWrapper)
+    
+    internal func loadDrawable() async throws -> SkeletonDrawableWrapper {
+        switch self {
+        case .bundle(let atlasFileName, let skeletonFileName, let bundle):
+            let atlasAndPages = try await Atlas.fromBundle(atlasFileName, bundle: bundle)
+            let skeletonData = try await SkeletonData.fromBundle(
+                atlas: atlasAndPages.0,
+                skeletonFileName: skeletonFileName,
+                bundle: bundle
+            )
+            return try SkeletonDrawableWrapper(
+                atlas: atlasAndPages.0,
+                atlasPages: atlasAndPages.1,
+                skeletonData: skeletonData
+            )
+        case .file(let atlasFile, let skeletonFile):
+            let atlasAndPages = try await Atlas.fromFile(atlasFile)
+            let skeletonData = try await SkeletonData.fromFile(
+                atlas: atlasAndPages.0,
+                skeletonFile: skeletonFile
+            )
+            return try SkeletonDrawableWrapper(
+                atlas: atlasAndPages.0,
+                atlasPages: atlasAndPages.1,
+                skeletonData: skeletonData
+            )
+        case .http(let atlasURL, let skeletonURL):
+            let atlasAndPages = try await Atlas.fromHttp(atlasURL)
+            let skeletonData = try await SkeletonData.fromHttp(
+                atlas: atlasAndPages.0,
+                skeletonURL: skeletonURL
+            )
+            return try SkeletonDrawableWrapper(
+                atlas: atlasAndPages.0,
+                atlasPages: atlasAndPages.1,
+                skeletonData: skeletonData
+            )
+        case .drawable(let skeletonDrawableWrapper):
+            return skeletonDrawableWrapper
+        }
+    }
+}

+ 76 - 0
spine-ios/Sources/Spine/SpineView.swift

@@ -0,0 +1,76 @@
+import SwiftUI
+
+/// A `SwiftUI` `View` to display a Spine skeleton. The skeleton can be loaded from a bundle, local files, http, or a pre-loaded ``SkeletonDrawableWrapper``.
+///
+/// The skeleton displayed by a ``SpineUIView`` can be controlled via a ``SpineController``.
+///
+/// The size of the widget can be derived from the bounds provided by a ``BoundsProvider``. If the view is not sized by the bounds
+/// computed by the ``BoundsProvider``, the widget will use the computed bounds to fit the skeleton inside the view's dimensions.
+///
+/// This is a ``UIViewRepresentable`` of `SpineUIView`.
+public struct SpineView: UIViewRepresentable {
+    
+    public typealias UIViewType = SpineUIView
+
+    private let source: SpineViewSource
+    private let controller: SpineController
+    private let mode: Spine.ContentMode
+    private let alignment: Spine.Alignment
+    private let boundsProvider: BoundsProvider
+    private let backgroundColor: UIColor // Not using `SwiftUI.Color`, as briging to `UIColor` prior iOS 14 might not always work.
+    
+    @Binding
+    private var isRendering: Bool?
+    
+    /// An initializer that constructs a new ``SpineView`` from a ``SpineViewSource``.
+    ///
+    /// After initialization is complete, the provided `controller` is invoked as per the ``SpineController`` semantics, to allow
+    /// modifying how the skeleton inside the widget is animated and rendered.
+    ///
+    /// - Parameters:
+    ///     - from: Specifies the ``SpineViewSource`` from which to load `atlas` and `skeleton` data.
+    ///     - controller: The ``SpineController`` used to modify how the skeleton inside the view is animated and rendered.
+    ///     - skeletonFileName: Specifies either a Skeleton `.json` or `.skel` file containing the skeleton data
+    ///     - bundle: Specifies from which bundle to load the files. Per default, it is `Bundle.main`
+    ///     - mode: How the skeleton is fitted inside ``SpineUIView``. Per default, it is `.fit`
+    ///     - alignment: How the skeleton is alignment inside ``SpineUIView``. Per default, it is `.center`
+    ///     - boundsProvider: The skeleton bounds must be computed via a ``BoundsProvider``. Per default, ``SetupPoseBounds`` is used.
+    ///     - backgroundColor: The background color of the view. Per defaut, `UIColor.clear` is used
+    ///     - isRendering: Bindgin to disable or enable rendering. Disable it when the spine view is out of bounds and you want to preserve CPU/GPU resources.
+    ///
+    /// - Returns: A new instance of ``SpineView``.
+    public init(
+        from source: SpineViewSource,
+        controller: SpineController = SpineController(),
+        mode: Spine.ContentMode = .fit,
+        alignment: Spine.Alignment = .center,
+        boundsProvider: BoundsProvider = SetupPoseBounds(),
+        backgroundColor: UIColor = .clear,
+        isRendering: Binding<Bool?> = .constant(nil)
+    ) {
+        self.source = source
+        self.controller = controller
+        self.mode = mode
+        self.alignment = alignment
+        self.boundsProvider = boundsProvider
+        self.backgroundColor = backgroundColor
+        _isRendering = isRendering
+    }
+    
+    public func makeUIView(context: Context) -> SpineUIView {
+        return SpineUIView(
+            from: source,
+            controller: controller,
+            mode: mode,
+            alignment: alignment,
+            boundsProvider: boundsProvider,
+            backgroundColor: backgroundColor
+        )
+    }
+    
+    public func updateUIView(_ uiView: SpineUIView, context: Context) {
+        if let isRendering {
+            uiView.isRendering = isRendering
+        }
+    }
+}

+ 4 - 0
spine-ios/Sources/SpineCppLite/include/module.modulemap

@@ -0,0 +1,4 @@
+module SpineCppLite {
+    header "spine-cpp-lite.h"
+    export *
+}

+ 1 - 0
spine-ios/Sources/SpineCppLite/include/spine

@@ -0,0 +1 @@
+./../../../../spine-cpp/spine-cpp/include/spine

+ 1 - 0
spine-ios/Sources/SpineCppLite/include/spine-cpp-lite.h

@@ -0,0 +1 @@
+../../../../spine-cpp/spine-cpp-lite/spine-cpp-lite.h

+ 1 - 0
spine-ios/Sources/SpineCppLite/spine

@@ -0,0 +1 @@
+./../../../spine-cpp/spine-cpp/src/spine

+ 1 - 0
spine-ios/Sources/SpineCppLite/spine-cpp-lite.cpp

@@ -0,0 +1 @@
+./../../../spine-cpp/spine-cpp-lite/spine-cpp-lite.cpp

+ 1 - 0
spine-ios/Sources/SpineShadersStructs/SpineShadersStructs.cpp

@@ -0,0 +1 @@
+#include "SpineShadersStructs.h"

+ 28 - 0
spine-ios/Sources/SpineShadersStructs/SpineShadersStructs.h

@@ -0,0 +1,28 @@
+#ifndef SpineShadersStructs_h
+#define SpineShadersStructs_h
+
+#include <simd/simd.h>
+
+typedef enum SpineVertexInputIndex {
+    SpineVertexInputIndexVertices     = 0,
+    SpineVertexInputIndexTransform    = 1,
+    SpineVertexInputIndexViewportSize = 2,
+} SpineVertexInputIndex;
+
+typedef enum SpineTextureIndex {
+    SpineTextureIndexBaseColor = 0,
+} SpineTextureIndex;
+
+typedef struct {
+    vector_float2 position;
+    vector_float4 color;
+    vector_float2 uv;
+} SpineVertex;
+
+typedef struct {
+    vector_float2 translation;
+    vector_float2 scale;
+    vector_float2 offset;
+} SpineTransform;
+
+#endif /* SpineShadersStructs_h */

+ 4 - 0
spine-ios/Sources/SpineShadersStructs/module.modulemap

@@ -0,0 +1,4 @@
+module SpineShadersStructs {
+    header "SpineShadersStructs.h"
+    export *
+}

+ 4 - 0
spine-ios/setup.sh

@@ -0,0 +1,4 @@
+#!/bin/bash
+set -e
+
+python3 ../spine-cpp/spine-cpp-lite/spine-cpp-lite-codegen.py > Sources/Spine/Spine.Generated.swift

部分文件因为文件数量过多而无法显示