feat(spine-ios): Update SpineiOS and Example to work with new SpineSwift generated bindings

- Remove AnimationStateWrapper (no longer needed with new SpineSwift API)
- Replace Spine.Generated+Extensions.swift with simplified SpineSwiftExtensions.swift
- Update SpineiOS to use SpineSwift API instead of direct SpineC calls
- Fix namespace conflicts (ContentMode → SpineContentMode, Alignment → SpineAlignment)
- Update texture mapping to use atlas page index from RenderCommand pointer
- Fix all Example app API calls to match new SpineSwift generated API:
  - setAnimationByName → setAnimation
  - addAnimationByName → addAnimation
  - slot.color → slot.appliedPose.color.set()
  - EventType enum cases instead of constants
  - Physics enum with qualified name to avoid conflicts
  - setSkin2() instead of property assignment
  - Array iteration using indices instead of for-in
  - bone.worldX → bone.appliedPose.worldX
- Update Objective-C imports from Spine to SpineiOS module

Note: SimpleAnimationViewController.m still needs updates for full ObjC compatibility
This commit is contained in:
Mario Zechner 2025-08-26 17:34:31 +02:00
parent 32a8552387
commit 9b345068b6
21 changed files with 197 additions and 493 deletions

View File

@ -13,6 +13,7 @@
76381FDB2C2EF4F10087712B /* spineboy-pma.atlas in Resources */ = {isa = PBXBuildFile; fileRef = 76381FD52C2EF4F00087712B /* spineboy-pma.atlas */; }; 76381FDB2C2EF4F10087712B /* spineboy-pma.atlas in Resources */ = {isa = PBXBuildFile; fileRef = 76381FD52C2EF4F00087712B /* spineboy-pma.atlas */; };
76381FDE2C2EF53A0087712B /* mix-and-match-pma.png in Resources */ = {isa = PBXBuildFile; fileRef = 76381FDC2C2EF53A0087712B /* mix-and-match-pma.png */; }; 76381FDE2C2EF53A0087712B /* mix-and-match-pma.png in Resources */ = {isa = PBXBuildFile; fileRef = 76381FDC2C2EF53A0087712B /* mix-and-match-pma.png */; };
76381FDF2C2EF53A0087712B /* mix-and-match-pma.atlas in Resources */ = {isa = PBXBuildFile; fileRef = 76381FDD2C2EF53A0087712B /* mix-and-match-pma.atlas */; }; 76381FDF2C2EF53A0087712B /* mix-and-match-pma.atlas in Resources */ = {isa = PBXBuildFile; fileRef = 76381FDD2C2EF53A0087712B /* mix-and-match-pma.atlas */; };
76DD66952E5DE75400963397 /* SpineiOS in Frameworks */ = {isa = PBXBuildFile; productRef = 76DD66942E5DE75400963397 /* SpineiOS */; };
9205FCD42C0760B1006EE07E /* SimpleAnimationViewControllerRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9205FCD32C0760B1006EE07E /* SimpleAnimationViewControllerRepresentable.swift */; }; 9205FCD42C0760B1006EE07E /* SimpleAnimationViewControllerRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9205FCD32C0760B1006EE07E /* SimpleAnimationViewControllerRepresentable.swift */; };
920BD1162BEBC52D0050A5A9 /* spineboy-pro.skel in Resources */ = {isa = PBXBuildFile; fileRef = 920BD1122BEBC52D0050A5A9 /* spineboy-pro.skel */; }; 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 */; }; 920BD1182BEBC52D0050A5A9 /* spineboy-pro.json in Resources */ = {isa = PBXBuildFile; fileRef = 920BD1142BEBC52D0050A5A9 /* spineboy-pro.json */; };
@ -33,10 +34,8 @@
924C0C182BCFCF21004E63F7 /* SimpleAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 924C0C172BCFCF21004E63F7 /* SimpleAnimation.swift */; }; 924C0C182BCFCF21004E63F7 /* SimpleAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 924C0C172BCFCF21004E63F7 /* SimpleAnimation.swift */; };
924C0C1A2BCFCF22004E63F7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 924C0C192BCFCF22004E63F7 /* Assets.xcassets */; }; 924C0C1A2BCFCF22004E63F7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 924C0C192BCFCF22004E63F7 /* Assets.xcassets */; };
92579E772C1B0E9500FDC7D5 /* DisableRendering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92579E762C1B0E9500FDC7D5 /* DisableRendering.swift */; }; 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 */; }; 9270C16E2BFE356000BD25BC /* Physics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9270C16D2BFE356000BD25BC /* Physics.swift */; };
9270C1742BFE389600BD25BC /* celestial-circus-pro.skel in Resources */ = {isa = PBXBuildFile; fileRef = 9270C1712BFE389600BD25BC /* celestial-circus-pro.skel */; }; 9270C1742BFE389600BD25BC /* celestial-circus-pro.skel in Resources */ = {isa = PBXBuildFile; fileRef = 9270C1712BFE389600BD25BC /* celestial-circus-pro.skel */; };
928A8CC22BCFE7DF00D9D35B /* Spine in Frameworks */ = {isa = PBXBuildFile; productRef = 928A8CC12BCFE7DF00D9D35B /* Spine */; };
92D7DDA82BFF3C8800EB9E3A /* DebugRendering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D7DDA72BFF3C8800EB9E3A /* DebugRendering.swift */; }; 92D7DDA82BFF3C8800EB9E3A /* DebugRendering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D7DDA72BFF3C8800EB9E3A /* DebugRendering.swift */; };
92FE93292BF4AB9600CCDF48 /* IKFollowing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92FE93282BF4AB9600CCDF48 /* IKFollowing.swift */; }; 92FE93292BF4AB9600CCDF48 /* IKFollowing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92FE93282BF4AB9600CCDF48 /* IKFollowing.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -83,8 +82,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
925CB7E92C19BC5A00C8F47F /* Spine in Frameworks */, 76DD66952E5DE75400963397 /* SpineiOS in Frameworks */,
928A8CC22BCFE7DF00D9D35B /* Spine in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -212,8 +210,7 @@
); );
name = "Spine iOS Example"; name = "Spine iOS Example";
packageProductDependencies = ( packageProductDependencies = (
928A8CC12BCFE7DF00D9D35B /* Spine */, 76DD66942E5DE75400963397 /* SpineiOS */,
925CB7E82C19BC5A00C8F47F /* Spine */,
); );
productName = "Spine iOS Example"; productName = "Spine iOS Example";
productReference = 924C0C122BCFCF21004E63F7 /* Spine iOS Example.app */; productReference = 924C0C122BCFCF21004E63F7 /* Spine iOS Example.app */;
@ -245,7 +242,7 @@
); );
mainGroup = 924C0C092BCFCF21004E63F7; mainGroup = 924C0C092BCFCF21004E63F7;
packageReferences = ( packageReferences = (
925CB7E72C19BC5A00C8F47F /* XCLocalSwiftPackageReference "../.." */, 76DD668F2E5DE75400963397 /* XCLocalSwiftPackageReference "../../../spine-runtimes" */,
); );
productRefGroup = 924C0C132BCFCF21004E63F7 /* Products */; productRefGroup = 924C0C132BCFCF21004E63F7 /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -536,20 +533,16 @@
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */ /* Begin XCLocalSwiftPackageReference section */
925CB7E72C19BC5A00C8F47F /* XCLocalSwiftPackageReference "../.." */ = { 76DD668F2E5DE75400963397 /* XCLocalSwiftPackageReference "../../../spine-runtimes" */ = {
isa = XCLocalSwiftPackageReference; isa = XCLocalSwiftPackageReference;
relativePath = ../..; relativePath = "../../../spine-runtimes";
}; };
/* End XCLocalSwiftPackageReference section */ /* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
925CB7E82C19BC5A00C8F47F /* Spine */ = { 76DD66942E5DE75400963397 /* SpineiOS */ = {
isa = XCSwiftPackageProductDependency; isa = XCSwiftPackageProductDependency;
productName = Spine; productName = SpineiOS;
};
928A8CC12BCFE7DF00D9D35B /* Spine */ = {
isa = XCSwiftPackageProductDependency;
productName = Spine;
}; };
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
}; };

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import Spine import SpineiOS
import SpineCppLite import SpineSwift
import SwiftUI import SwiftUI
struct AnimationStateEvents: View { struct AnimationStateEvents: View {
@ -38,25 +38,25 @@ struct AnimationStateEvents: View {
onInitialized: { controller in onInitialized: { controller in
controller.skeleton.scaleX = 0.5 controller.skeleton.scaleX = 0.5
controller.skeleton.scaleY = 0.5 controller.skeleton.scaleY = 0.5
controller.skeleton.findSlot(slotName: "gun")?.setColor(r: 1, g: 0, b: 0, a: 1) controller.skeleton.findSlot("gun")?.appliedPose.color.set(1, 0, 0, 1)
controller.animationStateData.defaultMix = 0.2 controller.animationStateData.defaultMix = 0.2
let walk = controller.animationState.setAnimationByName(trackIndex: 0, animationName: "walk", loop: true) let walk = controller.animationState.setAnimation(0, "walk", true)
controller.animationStateWrapper.setTrackEntryListener(entry: walk) { type, entry, event in walk.setListener { type, entry, event in
print("Walk animation event \(type)") print("Walk animation event \(type)")
} }
controller.animationState.addAnimationByName(trackIndex: 0, animationName: "jump", loop: false, delay: 2) controller.animationState.addAnimation(0, "jump", false, 2)
let run = controller.animationState.addAnimationByName(trackIndex: 0, animationName: "run", loop: true, delay: 0) let run = controller.animationState.addAnimation(0, "run", true, 0)
controller.animationStateWrapper.setTrackEntryListener(entry: run) { type, entry, event in run.setListener { type, entry, event in
print("Run animation event \(type)") print("Run animation event \(type)")
} }
controller.animationStateWrapper.setStateListener { type, entry, event in controller.animationState.setListener { type, entry, event in
if type == SPINE_EVENT_TYPE_EVENT, let event { if type == .event, let event {
print( print(
"User event: { name: \(event.data.name ?? "--"), intValue: \(event.intValue), floatValue: \(event.floatValue), stringValue: \(event.stringValue ?? "--") }" "User event: { name: \(event.data.name ?? "--"), intValue: \(event.intValue), floatValue: \(event.floatValue), stringValue: \(event.stringValue ?? "--") }"
) )
} }
} }
let current = controller.animationState.getCurrent(trackIndex: 0)?.animation.name ?? "--" let current = controller.animationState.getCurrent(0)?.animation.name ?? "--"
print("Current: \(current)") print("Current: \(current)")
} }
) )

View File

@ -27,7 +27,7 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import Spine import SpineiOS
import SwiftUI import SwiftUI
struct DebugRendering: View { struct DebugRendering: View {
@ -71,18 +71,16 @@ final class DebugRenderingModel: ObservableObject {
init() { init() {
controller = SpineController( controller = SpineController(
onInitialized: { controller in onInitialized: { controller in
controller.animationState.setAnimationByName( controller.animationState.setAnimation(0, "walk", true)
trackIndex: 0,
animationName: "walk",
loop: true
)
}, },
onAfterPaint: { onAfterPaint: {
[weak self] controller in [weak self] controller in
guard let self else { return } guard let self else { return }
boneRects = controller.drawable.skeleton.bones.map { bone in let bones = controller.drawable.skeleton.bones
boneRects = (0..<bones.count).compactMap { i -> BoneRect? in
guard let bone = bones[i] else { return nil }
let position = controller.fromSkeletonCoordinates( let position = controller.fromSkeletonCoordinates(
position: CGPointMake(CGFloat(bone.worldX), CGFloat(bone.worldY)) position: CGPointMake(CGFloat(bone.appliedPose.worldX), CGFloat(bone.appliedPose.worldY))
) )
return BoneRect( return BoneRect(
id: UUID(), id: UUID(),

View File

@ -27,7 +27,7 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import Spine import SpineiOS
import SwiftUI import SwiftUI
struct DisableRendering: View { struct DisableRendering: View {
@ -35,11 +35,7 @@ struct DisableRendering: View {
@StateObject @StateObject
var controller = SpineController( var controller = SpineController(
onInitialized: { controller in onInitialized: { controller in
controller.animationState.setAnimationByName( controller.animationState.setAnimation(0, "walk", true)
trackIndex: 0,
animationName: "walk",
loop: true
)
} }
) )

View File

@ -27,8 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import Spine import SpineiOS
import SpineCppLite import SpineSwift
import SwiftUI import SwiftUI
struct DressUp: View { struct DressUp: View {
@ -94,11 +94,7 @@ final class DressUpModel: ObservableObject {
init() { init() {
controller = SpineController( controller = SpineController(
onInitialized: { controller in onInitialized: { controller in
controller.animationState.setAnimationByName( controller.animationState.setAnimation(0, "dance", true)
trackIndex: 0,
animationName: "dance",
loop: true
)
}, },
disposeDrawableOnDeInit: false disposeDrawableOnDeInit: false
) )
@ -108,21 +104,22 @@ final class DressUpModel: ObservableObject {
skeletonFileName: "mix-and-match-pro.skel" skeletonFileName: "mix-and-match-pro.skel"
) )
try await MainActor.run { try await MainActor.run {
for skin in drawable.skeletonData.skins { let skins = drawable.skeletonData.skins
for i in 0..<skins.count {
guard let skin = skins[i] else { continue }
if skin.name == "default" { continue } if skin.name == "default" { continue }
let skeleton = drawable.skeleton let skeleton = drawable.skeleton
skeleton.skin = skin skeleton.setSkin2(skin)
skeleton.setToSetupPose() skeleton.setupPose()
skeleton.update(delta: 0) skeleton.update(0)
skeleton.updateWorldTransform(physics: SPINE_PHYSICS_UPDATE) skeleton.updateWorldTransform(SpineSwift.Physics.update)
try skin.name.flatMap { skinName in let skinName = skin.name
self.skinImages[skinName] = try drawable.renderToImage( self.skinImages[skinName] = try drawable.renderToImage(
size: self.thumbnailSize, size: self.thumbnailSize,
backgroundColor: .white, backgroundColor: .white,
scaleFactor: UIScreen.main.scale scaleFactor: UIScreen.main.scale
) )
self.selectedSkins[skinName] = false self.selectedSkins[skinName] = false
}
} }
self.toggleSkin(skinName: "full-skins/girl", drawable: drawable) self.toggleSkin(skinName: "full-skins/girl", drawable: drawable)
self.drawable = drawable self.drawable = drawable
@ -144,15 +141,15 @@ final class DressUpModel: ObservableObject {
func toggleSkin(skinName: String, drawable: SkeletonDrawableWrapper) { func toggleSkin(skinName: String, drawable: SkeletonDrawableWrapper) {
selectedSkins[skinName] = !(selectedSkins[skinName] ?? false) selectedSkins[skinName] = !(selectedSkins[skinName] ?? false)
customSkin?.dispose() customSkin?.dispose()
customSkin = Skin.create(name: "custom-skin") customSkin = Skin("custom-skin")
for skinName in selectedSkins.keys { for skinName in selectedSkins.keys {
if selectedSkins[skinName] == true { if selectedSkins[skinName] == true {
if let skin = drawable.skeletonData.findSkin(name: skinName) { if let skin = drawable.skeletonData.findSkin(skinName) {
customSkin?.addSkin(other: skin) customSkin?.addSkin(skin)
} }
} }
} }
drawable.skeleton.skin = customSkin drawable.skeleton.setSkin2(customSkin)
drawable.skeleton.setToSetupPose() drawable.skeleton.setupPose()
} }
} }

View File

@ -27,7 +27,7 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import Spine import SpineiOS
import SwiftUI import SwiftUI
struct IKFollowing: View { struct IKFollowing: View {
@ -74,16 +74,8 @@ final class IKFollowingModel: ObservableObject {
init() { init() {
controller = SpineController( controller = SpineController(
onInitialized: { controller in onInitialized: { controller in
controller.animationState.setAnimationByName( controller.animationState.setAnimation(0, "walk", true)
trackIndex: 0, controller.animationState.setAnimation(1, "aim", true)
animationName: "walk",
loop: true
)
controller.animationState.setAnimationByName(
trackIndex: 1,
animationName: "aim",
loop: true
)
}, },
onAfterUpdateWorldTransforms: { onAfterUpdateWorldTransforms: {
[weak self] controller in [weak self] controller in
@ -91,11 +83,11 @@ final class IKFollowingModel: ObservableObject {
guard let worldPosition = self.crossHairPosition else { guard let worldPosition = self.crossHairPosition else {
return return
} }
let bone = controller.skeleton.findBone(boneName: "crosshair")! let bone = controller.skeleton.findBone("crosshair")!
if let parent = bone.parent { if let parent = bone.parent {
let position = parent.worldToLocal(worldX: Float(worldPosition.x), worldY: Float(worldPosition.y)) let position = parent.appliedPose.worldToLocal(worldX: Float(worldPosition.x), worldY: Float(worldPosition.y))
bone.x = position.x bone.appliedPose.x = position.x
bone.y = position.y bone.appliedPose.y = position.y
} }
} }
) )

View File

@ -27,7 +27,7 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import Spine import SpineiOS
import SwiftUI import SwiftUI
struct MainView: View { struct MainView: View {
@ -72,7 +72,7 @@ struct MainView: View {
} footer: { } footer: {
HStack { HStack {
Spacer() Spacer()
Text("Spine \(Spine.version)") Text("Spine \(SpineiOS.version)")
.font(.footnote) .font(.footnote)
.foregroundColor(.secondary) .foregroundColor(.secondary)
Spacer() Spacer()

View File

@ -27,7 +27,7 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import Spine import SpineiOS
import SwiftUI import SwiftUI
struct Physics: View { struct Physics: View {
@ -72,16 +72,8 @@ final class PhysicsModel: ObservableObject {
init() { init() {
controller = SpineController( controller = SpineController(
onInitialized: { controller in onInitialized: { controller in
controller.animationState.setAnimationByName( controller.animationState.setAnimation(0, "eyeblink", true)
trackIndex: 0, controller.animationState.setAnimation(1, "wings-and-feet", true)
animationName: "eyeblink",
loop: true
)
controller.animationState.setAnimationByName(
trackIndex: 1,
animationName: "wings-and-feet",
loop: true
)
}, },
onAfterUpdateWorldTransforms: { onAfterUpdateWorldTransforms: {
[weak self] controller in [weak self] controller in
@ -98,7 +90,7 @@ final class PhysicsModel: ObservableObject {
let dy = mousePosition.y - lastMousePosition.y let dy = mousePosition.y - lastMousePosition.y
let positionX = controller.skeleton.x + Float(dx) let positionX = controller.skeleton.x + Float(dx)
let positionY = controller.skeleton.y + Float(dy) let positionY = controller.skeleton.y + Float(dy)
controller.skeleton.setPosition(x: positionX, y: positionY) controller.skeleton.setPosition(positionX, positionY)
self.lastMousePosition = mousePosition self.lastMousePosition = mousePosition
} }
) )

View File

@ -27,7 +27,7 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import Spine import SpineiOS
import SwiftUI import SwiftUI
struct PlayPauseAnimation: View { struct PlayPauseAnimation: View {
@ -35,11 +35,7 @@ struct PlayPauseAnimation: View {
@StateObject @StateObject
var controller = SpineController( var controller = SpineController(
onInitialized: { controller in onInitialized: { controller in
controller.animationState.setAnimationByName( controller.animationState.setAnimation(0, "flying", true)
trackIndex: 0,
animationName: "flying",
loop: true
)
} }
) )

View File

@ -27,7 +27,7 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import Spine import SpineiOS
import SwiftUI import SwiftUI
struct SimpleAnimation: View { struct SimpleAnimation: View {
@ -35,11 +35,7 @@ struct SimpleAnimation: View {
@StateObject @StateObject
var controller = SpineController( var controller = SpineController(
onInitialized: { controller in onInitialized: { controller in
controller.animationState.setAnimationByName( controller.animationState.setAnimation(0, "walk", true)
trackIndex: 0,
animationName: "walk",
loop: true
)
} }
) )

View File

@ -28,7 +28,7 @@
*****************************************************************************/ *****************************************************************************/
#import "SimpleAnimationViewController.h" #import "SimpleAnimationViewController.h"
@import Spine; @import SpineiOS;
@interface SimpleAnimationViewController () @interface SimpleAnimationViewController ()

View File

@ -27,7 +27,7 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import Spine import SpineiOS
import SwiftUI import SwiftUI
@main @main

View File

@ -1,99 +0,0 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import Foundation
import SpineSwift
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
}
}

View File

@ -29,6 +29,7 @@
import CoreGraphics import CoreGraphics
import Foundation import Foundation
import SpineSwift
/// Base class for bounds providers. A bounds provider calculates the axis aligned bounding box /// 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``. /// used to scale and fit a skeleton inside the bounds of a ``SpineUIView``.
@ -102,23 +103,22 @@ public final class SkinAndAnimationBounds: NSObject, BoundsProvider {
public func computeBounds(for drawable: SkeletonDrawableWrapper) -> CGRect { public func computeBounds(for drawable: SkeletonDrawableWrapper) -> CGRect {
let data = drawable.skeletonData let data = drawable.skeletonData
let oldSkin: Skin? = drawable.skeleton.skin let oldSkin: Skin? = drawable.skeleton.skin
let customSkin = Skin.create(name: "custom-skin") let customSkin = Skin("custom-skin") // Use constructor directly
for skinName in skins { for skinName in skins {
let skin = data.findSkin(name: skinName) if let skin = data.findSkin(skinName) {
if let skin = data.findSkin(name: skinName) { customSkin.addSkin(skin)
customSkin.addSkin(other: skin)
} }
} }
drawable.skeleton.skin = customSkin drawable.skeleton.setSkin2(customSkin)
drawable.skeleton.setToSetupPose() drawable.skeleton.setupPose()
let animation = animation.flatMap { data.findAnimation(name: $0) } let animation = animation.flatMap { data.findAnimation($0) }
var minX = Float.Magnitude.greatestFiniteMagnitude var minX = Float.Magnitude.greatestFiniteMagnitude
var minY = Float.Magnitude.greatestFiniteMagnitude var minY = Float.Magnitude.greatestFiniteMagnitude
var maxX = -Float.Magnitude.greatestFiniteMagnitude var maxX = -Float.Magnitude.greatestFiniteMagnitude
var maxY = -Float.Magnitude.greatestFiniteMagnitude var maxY = -Float.Magnitude.greatestFiniteMagnitude
if let animation { if let animation {
drawable.animationState.setAnimation(trackIndex: 0, animation: animation, loop: false) _ = drawable.animationState.setAnimation2(0, animation, false)
let steps = Int(max(Double(animation.duration) / stepTime, 1.0)) let steps = Int(max(Double(animation.duration) / stepTime, 1.0))
for i in 0..<steps { for i in 0..<steps {
drawable.update(delta: i > 0 ? Float(stepTime) : 0.0) drawable.update(delta: i > 0 ? Float(stepTime) : 0.0)
@ -135,13 +135,13 @@ public final class SkinAndAnimationBounds: NSObject, BoundsProvider {
maxX = minX + bounds.width maxX = minX + bounds.width
maxY = minY + bounds.height maxY = minY + bounds.height
} }
drawable.skeleton.setSkinByName(skinName: "default") drawable.skeleton.setSkin("default")
drawable.animationState.clearTracks() drawable.animationState.clearTracks()
if let oldSkin { if let oldSkin {
drawable.skeleton.skin = oldSkin drawable.skeleton.setSkin2(oldSkin)
} }
drawable.skeleton.setToSetupPose() drawable.skeleton.setupPose()
drawable.update(delta: 0) drawable.update(delta: 0)
customSkin.dispose() customSkin.dispose()
return CGRectMake(CGFloat(minX), CGFloat(minY), CGFloat(maxX - minX), CGFloat(maxY - minY)) return CGRectMake(CGFloat(minX), CGFloat(minY), CGFloat(maxX - minX), CGFloat(maxY - minY))
@ -150,7 +150,7 @@ public final class SkinAndAnimationBounds: NSObject, BoundsProvider {
/// How a view should be inscribed into another view. /// How a view should be inscribed into another view.
@objc @objc
public enum ContentMode: Int { public enum SpineContentMode: Int {
case fit case fit
/// As large as possible while still containing the source view entirely within the target view. /// 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. case fill/// Fill the target view by distorting the source's aspect ratio.
@ -158,7 +158,7 @@ public enum ContentMode: Int {
/// How a view should aligned withing another view. /// How a view should aligned withing another view.
@objc @objc
public enum Alignment: Int { public enum SpineAlignment: Int {
case topLeft case topLeft
case topCenter case topCenter
case topRight case topRight

View File

@ -28,6 +28,7 @@
*****************************************************************************/ *****************************************************************************/
import Foundation import Foundation
import SpineSwift
import SpineShadersStructs import SpineShadersStructs
import simd import simd
@ -35,19 +36,26 @@ extension RenderCommand {
func getVertices() -> [SpineVertex] { func getVertices() -> [SpineVertex] {
var vertices = [SpineVertex]() var vertices = [SpineVertex]()
let indices = indices let numVerts = Int(numVertices)
let numVertices = numVertices let numInds = Int(numIndices)
let positions = positions(numVertices: numVertices) guard let indicesPtr = indices,
let uvs = uvs(numVertices: numVertices) let positionsPtr = positions,
let colors = colors(numVertices: numVertices) let uvsPtr = uvs,
vertices.reserveCapacity(indices.count) let colorsPtr = colors else {
for i in 0..<indices.count { return vertices
let index = Int(indices[i]) }
let indicesArray = Array(UnsafeBufferPointer(start: indicesPtr, count: numInds))
let positionsArray = Array(UnsafeBufferPointer(start: positionsPtr, count: numVerts * 2))
let uvsArray = Array(UnsafeBufferPointer(start: uvsPtr, count: numVerts * 2))
let colorsArray = Array(UnsafeBufferPointer(start: colorsPtr, count: numVerts))
vertices.reserveCapacity(numInds)
for i in 0..<numInds {
let index = Int(indicesArray[i])
let xIndex = 2 * index let xIndex = 2 * index
let yIndex = xIndex + 1 let yIndex = xIndex + 1
let position = SIMD2<Float>(positions[xIndex], positions[yIndex]) let position = SIMD2<Float>(positionsArray[xIndex], positionsArray[yIndex])
let uv = SIMD2<Float>(uvs[xIndex], uvs[yIndex]) let uv = SIMD2<Float>(uvsArray[xIndex], uvsArray[yIndex])
let color = extractRGBA(from: colors[index]) let color = extractRGBA(from: colorsArray[index])
let vertex = SpineVertex( let vertex = SpineVertex(
position: position, position: position,
color: color, color: color,
@ -59,10 +67,7 @@ extension RenderCommand {
return vertices return vertices
} }
private func extractRGBA(from color: Int32) -> SIMD4<Float> { private func extractRGBA(from color: UInt32) -> SIMD4<Float> {
guard color != -1 else {
return SIMD4<Float>(1.0, 1.0, 1.0, 1.0)
}
let alpha = Float((color >> 24) & 0xFF) / 255.0 let alpha = Float((color >> 24) & 0xFF) / 255.0
let red = Float((color >> 16) & 0xFF) / 255.0 let red = Float((color >> 16) & 0xFF) / 255.0
let green = Float((color >> 8) & 0xFF) / 255.0 let green = Float((color >> 8) & 0xFF) / 255.0

View File

@ -31,6 +31,7 @@ import Foundation
import MetalKit import MetalKit
import SpineSwift import SpineSwift
import SpineC import SpineC
import SpineShadersStructs
protocol SpineRendererDelegate: AnyObject { protocol SpineRendererDelegate: AnyObject {
func spineRendererWillUpdate(_ spineRenderer: SpineRenderer) func spineRendererWillUpdate(_ spineRenderer: SpineRenderer)
@ -108,11 +109,11 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
) )
} }
let blendModes = [ let blendModes: [BlendMode] = [
SPINE_BLEND_MODE_NORMAL, .normal,
SPINE_BLEND_MODE_ADDITIVE, .additive,
SPINE_BLEND_MODE_MULTIPLY, .multiply,
SPINE_BLEND_MODE_SCREEN, .screen,
] ]
for blendMode in blendModes { for blendMode in blendModes {
let descriptor = MTLRenderPipelineDescriptor() let descriptor = MTLRenderPipelineDescriptor()
@ -185,7 +186,7 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
} }
} }
private func setTransform(bounds: CGRect, mode: Spine.ContentMode, alignment: Spine.Alignment) { private func setTransform(bounds: CGRect, mode: SpineContentMode, alignment: SpineAlignment) {
let x = -bounds.minX - bounds.width / 2.0 let x = -bounds.minX - bounds.width / 2.0
let y = -bounds.minY - bounds.height / 2.0 let y = -bounds.minY - bounds.height / 2.0
@ -290,7 +291,8 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
let vertices = allVertices[index] let vertices = allVertices[index]
let textureIndex = Int(renderCommand.atlasPage) // When using spine_atlas_load, texture is actually the atlas page index cast as a pointer
let textureIndex = Int(bitPattern: renderCommand.texture)
if textures.indices.contains(textureIndex) { if textures.indices.contains(textureIndex) {
renderEncoder.setFragmentTexture( renderEncoder.setFragmentTexture(
textures[textureIndex], textures[textureIndex],
@ -321,63 +323,55 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
extension BlendMode { extension BlendMode {
fileprivate func sourceRGBBlendFactor(premultipliedAlpha: Bool) -> MTLBlendFactor { fileprivate func sourceRGBBlendFactor(premultipliedAlpha: Bool) -> MTLBlendFactor {
switch self { switch self {
case SPINE_BLEND_MODE_NORMAL: case .normal:
return premultipliedAlpha ? .one : .sourceAlpha return premultipliedAlpha ? .one : .sourceAlpha
case SPINE_BLEND_MODE_ADDITIVE: case .additive:
// additvie only needs sourceAlpha multiply if it is not pma // additvie only needs sourceAlpha multiply if it is not pma
return premultipliedAlpha ? .one : .sourceAlpha return premultipliedAlpha ? .one : .sourceAlpha
case SPINE_BLEND_MODE_MULTIPLY: case .multiply:
return .destinationColor return .destinationColor
case SPINE_BLEND_MODE_SCREEN: case .screen:
return .one return .one
default:
return .one // Should never be called
} }
} }
fileprivate var sourceAlphaBlendFactor: MTLBlendFactor { fileprivate var sourceAlphaBlendFactor: MTLBlendFactor {
// pma and non-pma has no-relation ship with alpha blending // pma and non-pma has no-relation ship with alpha blending
switch self { switch self {
case SPINE_BLEND_MODE_NORMAL: case .normal:
return .one return .one
case SPINE_BLEND_MODE_ADDITIVE: case .additive:
return .one return .one
case SPINE_BLEND_MODE_MULTIPLY: case .multiply:
return .oneMinusSourceAlpha return .oneMinusSourceAlpha
case SPINE_BLEND_MODE_SCREEN: case .screen:
return .oneMinusSourceColor return .oneMinusSourceColor
default:
return .one // Should never be called
} }
} }
fileprivate var destinationRGBBlendFactor: MTLBlendFactor { fileprivate var destinationRGBBlendFactor: MTLBlendFactor {
switch self { switch self {
case SPINE_BLEND_MODE_NORMAL: case .normal:
return .oneMinusSourceAlpha return .oneMinusSourceAlpha
case SPINE_BLEND_MODE_ADDITIVE: case .additive:
return .one return .one
case SPINE_BLEND_MODE_MULTIPLY: case .multiply:
return .oneMinusSourceAlpha return .oneMinusSourceAlpha
case SPINE_BLEND_MODE_SCREEN: case .screen:
return .oneMinusSourceColor return .oneMinusSourceColor
default:
return .one // Should never be called
} }
} }
fileprivate var destinationAlphaBlendFactor: MTLBlendFactor { fileprivate var destinationAlphaBlendFactor: MTLBlendFactor {
switch self { switch self {
case SPINE_BLEND_MODE_NORMAL: case .normal:
return .oneMinusSourceAlpha return .oneMinusSourceAlpha
case SPINE_BLEND_MODE_ADDITIVE: case .additive:
return .one return .one
case SPINE_BLEND_MODE_MULTIPLY: case .multiply:
return .oneMinusSourceAlpha return .oneMinusSourceAlpha
case SPINE_BLEND_MODE_SCREEN: case .screen:
return .oneMinusSourceColor return .oneMinusSourceColor
default:
return .one // Should never be called
} }
} }
} }

View File

@ -62,7 +62,6 @@ public final class SkeletonDrawableWrapper: NSObject {
public let skeleton: Skeleton public let skeleton: Skeleton
public let animationStateData: AnimationStateData public let animationStateData: AnimationStateData
public let animationState: AnimationState public let animationState: AnimationState
public let animationStateWrapper: AnimationStateWrapper
internal var disposed = false internal var disposed = false
@ -121,30 +120,12 @@ public final class SkeletonDrawableWrapper: NSObject {
self.atlasPages = atlasPages self.atlasPages = atlasPages
self.skeletonData = skeletonData self.skeletonData = skeletonData
guard let nativeSkeletonDrawable = spine_skeleton_drawable_create(skeletonData.wrappee) else { skeletonDrawable = SkeletonDrawable(skeletonData: skeletonData)
throw SpineError("Could not load native skeleton drawable")
}
skeletonDrawable = SkeletonDrawable(nativeSkeletonDrawable)
guard let nativeSkeleton = spine_skeleton_drawable_get_skeleton(skeletonDrawable.wrappee) else { skeleton = skeletonDrawable.skeleton
throw SpineError("Could not load native skeleton") animationStateData = skeletonDrawable.animationStateData
} animationState = skeletonDrawable.animationState
skeleton = Skeleton(nativeSkeleton) skeleton.updateWorldTransform(Physics.none)
guard let nativeAnimationStateData = spine_skeleton_drawable_get_animation_state_data(skeletonDrawable.wrappee) else {
throw SpineError("Could not load native animation state data")
}
animationStateData = AnimationStateData(nativeAnimationStateData)
guard let nativeAnimationState = spine_skeleton_drawable_get_animation_state(skeletonDrawable.wrappee) else {
throw SpineError("Could not load native animation state")
}
animationState = AnimationState(nativeAnimationState)
animationStateWrapper = AnimationStateWrapper(
animationState: animationState,
aninationStateEvents: skeletonDrawable.animationStateEvents
)
skeleton.updateWorldTransform(physics: SPINE_PHYSICS_NONE)
super.init() super.init()
} }
@ -154,11 +135,11 @@ public final class SkeletonDrawableWrapper: NSObject {
public func update(delta: Float) { public func update(delta: Float) {
if disposed { return } if disposed { return }
animationStateWrapper.update(delta: delta) animationState.update(delta)
animationState.apply(skeleton: skeleton) _ = animationState.apply(skeleton)
skeleton.update(delta: delta) skeleton.update(delta)
skeleton.updateWorldTransform(physics: SPINE_PHYSICS_UPDATE) skeleton.updateWorldTransform(Physics.update)
} }
public func dispose() { public func dispose() {
@ -168,6 +149,6 @@ public final class SkeletonDrawableWrapper: NSObject {
atlas.dispose() atlas.dispose()
skeletonData.dispose() skeletonData.dispose()
skeletonDrawable.dispose() // SkeletonDrawable disposal handled by ARC
} }
} }

View File

@ -31,6 +31,7 @@ import CoreGraphics
import Foundation import Foundation
import QuartzCore import QuartzCore
import UIKit import UIKit
import SpineSwift
public typealias SpineControllerCallback = (_ controller: SpineController) -> Void public typealias SpineControllerCallback = (_ controller: SpineController) -> Void
@ -136,10 +137,6 @@ public final class SpineController: NSObject, ObservableObject {
drawable.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 /// 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 /// the skeleton coordinate system. See the `IKFollowing.swift` example how to use this
@ -239,6 +236,14 @@ extension SpineController: SpineRendererDataSource {
} }
func renderCommands(_ spineRenderer: SpineRenderer) -> [RenderCommand] { func renderCommands(_ spineRenderer: SpineRenderer) -> [RenderCommand] {
return drawable?.skeletonDrawable.render() ?? [] guard let drawable = drawable else { return [] }
var commands = [RenderCommand]()
var current = drawable.skeletonDrawable.render()
while let cmd = current {
commands.append(cmd)
current = cmd.next
}
return commands
} }
} }

View File

@ -30,24 +30,26 @@
import Foundation import Foundation
import SpineSwift import SpineSwift
import SwiftUI import SwiftUI
import UIKit
// Re-export version info from SpineSwift
public var version: String { public var version: String {
return "\(majorVersion).\(minorVersion)" return SpineSwift.version
} }
public var majorVersion: Int { public var majorVersion: Int {
return Int(spine_major_version()) return SpineSwift.majorVersion
} }
public var minorVersion: Int { public var minorVersion: Int {
return Int(spine_minor_version()) return SpineSwift.minorVersion
} }
/// ``Atlas`` data loaded from a `.atlas` file and its corresponding `.png` files. For each atlas image, /// ``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 /// a corresponding `UIImage` is constructed, which is used when rendering a skeleton
/// that uses this atlas. /// that uses this atlas.
/// ///
/// Use the static methods ``Atlas/fromBundle(_:bundle:)``, ``Atlas/fromFile(_:)``, and ``Atlas/fromHttp(_:)`` to load an atlas. Call ``Atlas/dispose()` /// 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. /// when the atlas is no longer in use to release its resources.
extension Atlas { extension Atlas {
@ -89,34 +91,26 @@ extension Atlas {
guard let atlasData = String(data: data, encoding: .utf8) else { guard let atlasData = String(data: data, encoding: .utf8) else {
throw SpineError("Couldn't read atlas bytes as utf8 string") throw SpineError("Couldn't read atlas bytes as utf8 string")
} }
let atlas = try atlasData.utf8CString.withUnsafeBufferPointer {
guard let atlas = spine_atlas_load($0.baseAddress) else { // Use SpineSwift's loadAtlas function
throw SpineError("Couldn't load atlas data") let atlas = try loadAtlas(atlasData)
}
return atlas
}
if let error = spine_atlas_get_error(atlas) {
let message = String(cString: error)
spine_atlas_dispose(atlas)
throw SpineError("Couldn't load atlas: \(message)")
}
var atlasPages = [UIImage]() var atlasPages = [UIImage]()
let numImagePaths = spine_atlas_get_num_image_paths(atlas)
// Load images for each atlas page
for i in 0..<numImagePaths { let pages = atlas.pages
guard let atlasPageFilePointer = spine_atlas_get_image_path(atlas, i) else { for i in 0..<pages.count {
continue guard let page = pages[i] else { continue }
} let imagePath = page.texturePath
let atlasPageFile = String(cString: atlasPageFilePointer)
let imageData = try await loadFile(atlasPageFile) let imageData = try await loadFile(imagePath)
guard let image = UIImage(data: imageData) else { guard let image = UIImage(data: imageData) else {
continue continue
} }
atlasPages.append(image) atlasPages.append(image)
} }
return (Atlas(atlas), atlasPages) return (atlas, atlasPages)
} }
} }
@ -130,7 +124,8 @@ extension SkeletonData {
return try fromData( return try fromData(
atlas: atlas, atlas: atlas,
data: try await FileSource.bundle(fileName: skeletonFileName, bundle: bundle).load(), data: try await FileSource.bundle(fileName: skeletonFileName, bundle: bundle).load(),
isJson: skeletonFileName.hasSuffix(".json") isJson: skeletonFileName.hasSuffix(".json"),
path: skeletonFileName
) )
} }
@ -141,7 +136,8 @@ extension SkeletonData {
return try fromData( return try fromData(
atlas: atlas, atlas: atlas,
data: try await FileSource.file(skeletonFile).load(), data: try await FileSource.file(skeletonFile).load(),
isJson: skeletonFile.absoluteString.hasSuffix(".json") isJson: skeletonFile.absoluteString.hasSuffix(".json"),
path: skeletonFile.absoluteString
) )
} }
@ -152,141 +148,23 @@ extension SkeletonData {
return try fromData( return try fromData(
atlas: atlas, atlas: atlas,
data: try await FileSource.http(skeletonURL).load(), data: try await FileSource.http(skeletonURL).load(),
isJson: skeletonURL.absoluteString.hasSuffix(".json") isJson: skeletonURL.absoluteString.hasSuffix(".json"),
path: skeletonURL.absoluteString
) )
} }
/// Loads a ``SkeletonData`` from the ``binary`` skeleton `Data`, using the provided ``Atlas`` to resolve attachment images. private static func fromData(atlas: Atlas, data: Data, isJson: Bool, path: String) throws -> SkeletonData {
///
/// Throws an `Error` in case the skeleton data could not be loaded.
public static func fromData(atlas: Atlas, data: Data) throws -> SkeletonData {
let result = try data.withUnsafeBytes {
try $0.withMemoryRebound(to: UInt8.self) { buffer in
guard let ptr = buffer.baseAddress else {
throw SpineError("Couldn't read atlas binary")
}
return spine_skeleton_data_load_binary(
atlas.wrappee,
ptr,
Int32(buffer.count)
)
}
}
guard let result else {
throw SpineError("Couldn't load skeleton data")
}
defer {
spine_skeleton_data_result_dispose(result)
}
if let error = spine_skeleton_data_result_get_error(result) {
let message = String(cString: error)
throw SpineError("Couldn't load skeleton data: \(message)")
}
guard let data = spine_skeleton_data_result_get_data(result) else {
throw SpineError("Couldn't load skeleton data from result")
}
return SkeletonData(data)
}
/// Loads a ``SkeletonData`` from the `json` string, using the provided ``Atlas`` to resolve attachment
/// images.
///
/// Throws an `Error` in case the atlas could not be loaded.
public static func fromJson(atlas: Atlas, json: String) throws -> SkeletonData {
let result = try json.utf8CString.withUnsafeBufferPointer { buffer in
guard
let basePtr = buffer.baseAddress,
let result = spine_skeleton_data_load_json(atlas.wrappee, basePtr)
else {
throw SpineError("Couldn't load skeleton data json")
}
return result
}
defer {
spine_skeleton_data_result_dispose(result)
}
if let error = spine_skeleton_data_result_get_error(result) {
let message = String(cString: error)
throw SpineError("Couldn't load skeleton data: \(message)")
}
guard let data = spine_skeleton_data_result_get_data(result) else {
throw SpineError("Couldn't load skeleton data from result")
}
return SkeletonData(data)
}
private static func fromData(atlas: Atlas, data: Data, isJson: Bool) throws -> SkeletonData {
if isJson { if isJson {
guard let json = String(data: data, encoding: .utf8) else { guard let json = String(data: data, encoding: .utf8) else {
throw SpineError("Couldn't read skeleton data json string") throw SpineError("Couldn't read skeleton data json string")
} }
return try fromJson(atlas: atlas, json: json) return try loadSkeletonDataJson(atlas: atlas, jsonData: json, path: path)
} else { } else {
return try fromData(atlas: atlas, data: data) return try loadSkeletonDataBinary(atlas: atlas, binaryData: data, path: path)
} }
} }
} }
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
}
}
extension RenderCommand {
var numVertices: Int {
Int(spine_render_command_get_num_vertices(wrappee))
}
func positions(numVertices: Int) -> [Float] {
let num = numVertices * 2
let ptr = spine_render_command_get_positions(wrappee)
guard let validPtr = ptr else { return [] }
let buffer = UnsafeBufferPointer(start: validPtr, count: num)
return Array(buffer)
}
func uvs(numVertices: Int) -> [Float] {
let num = numVertices * 2
let ptr = spine_render_command_get_uvs(wrappee)
guard let validPtr = ptr else { return [] }
let buffer = UnsafeBufferPointer(start: validPtr, count: num)
return Array(buffer)
}
func colors(numVertices: Int) -> [Int32] {
let num = numVertices
let ptr = spine_render_command_get_colors(wrappee)
guard let validPtr = ptr else { return [] }
let buffer = UnsafeBufferPointer(start: validPtr, count: num)
return Array(buffer)
}
}
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.
public static func create(name: String) -> Skin {
return Skin(spine_skin_create(name))
}
}
// Helper // Helper
extension CGRect { extension CGRect {
@ -380,26 +258,5 @@ internal enum FileSource {
} }
} }
public struct SpineError: Error, CustomStringConvertible { // Re-export SpineError from SpineSwift
public typealias SpineError = SpineSwift.SpineError
public let description: String
internal init(_ description: String) {
self.description = description
}
}
extension SkeletonBounds {
public static func create() -> SkeletonBounds {
return SkeletonBounds(spine_skeleton_bounds_create())
}
}
@objc extension Atlas {
public var imagePathCount: Int32 {
spine_atlas_get_num_image_paths(wrappee)
}
}

View File

@ -29,6 +29,7 @@
import MetalKit import MetalKit
import UIKit import UIKit
import SpineSwift
/// A ``UIView`` to display a Spine skeleton. The skeleton can be loaded from a bundle, local files, http, or a pre-loaded ``SkeletonDrawableWrapper``. /// A ``UIView`` to display a Spine skeleton. The skeleton can be loaded from a bundle, local files, http, or a pre-loaded ``SkeletonDrawableWrapper``.
/// ///
@ -42,8 +43,8 @@ import UIKit
public final class SpineUIView: MTKView { public final class SpineUIView: MTKView {
let controller: SpineController let controller: SpineController
let mode: Spine.ContentMode let mode: SpineContentMode
let alignment: Spine.Alignment let alignment: SpineAlignment
let boundsProvider: BoundsProvider let boundsProvider: BoundsProvider
internal var computedBounds: CGRect = .zero internal var computedBounds: CGRect = .zero
@ -51,8 +52,8 @@ public final class SpineUIView: MTKView {
@objc internal init( @objc internal init(
controller: SpineController = SpineController(), controller: SpineController = SpineController(),
mode: Spine.ContentMode = .fit, mode: SpineContentMode = .fit,
alignment: Spine.Alignment = .center, alignment: SpineAlignment = .center,
boundsProvider: BoundsProvider = SetupPoseBounds(), boundsProvider: BoundsProvider = SetupPoseBounds(),
backgroundColor: UIColor = .clear backgroundColor: UIColor = .clear
) { ) {
@ -83,8 +84,8 @@ public final class SpineUIView: MTKView {
public convenience init( public convenience init(
from source: SpineViewSource, from source: SpineViewSource,
controller: SpineController = SpineController(), controller: SpineController = SpineController(),
mode: Spine.ContentMode = .fit, mode: SpineContentMode = .fit,
alignment: Spine.Alignment = .center, alignment: SpineAlignment = .center,
boundsProvider: BoundsProvider = SetupPoseBounds(), boundsProvider: BoundsProvider = SetupPoseBounds(),
backgroundColor: UIColor = .clear backgroundColor: UIColor = .clear
) { ) {
@ -120,8 +121,8 @@ public final class SpineUIView: MTKView {
skeletonFileName: String, skeletonFileName: String,
bundle: Bundle = .main, bundle: Bundle = .main,
controller: SpineController = SpineController(), controller: SpineController = SpineController(),
mode: Spine.ContentMode = .fit, mode: SpineContentMode = .fit,
alignment: Spine.Alignment = .center, alignment: SpineAlignment = .center,
boundsProvider: BoundsProvider = SetupPoseBounds(), boundsProvider: BoundsProvider = SetupPoseBounds(),
backgroundColor: UIColor = .clear backgroundColor: UIColor = .clear
) { ) {
@ -149,8 +150,8 @@ public final class SpineUIView: MTKView {
atlasFile: URL, atlasFile: URL,
skeletonFile: URL, skeletonFile: URL,
controller: SpineController = SpineController(), controller: SpineController = SpineController(),
mode: Spine.ContentMode = .fit, mode: SpineContentMode = .fit,
alignment: Spine.Alignment = .center, alignment: SpineAlignment = .center,
boundsProvider: BoundsProvider = SetupPoseBounds(), boundsProvider: BoundsProvider = SetupPoseBounds(),
backgroundColor: UIColor = .clear backgroundColor: UIColor = .clear
) { ) {
@ -178,8 +179,8 @@ public final class SpineUIView: MTKView {
atlasURL: URL, atlasURL: URL,
skeletonURL: URL, skeletonURL: URL,
controller: SpineController = SpineController(), controller: SpineController = SpineController(),
mode: Spine.ContentMode = .fit, mode: SpineContentMode = .fit,
alignment: Spine.Alignment = .center, alignment: SpineAlignment = .center,
boundsProvider: BoundsProvider = SetupPoseBounds(), boundsProvider: BoundsProvider = SetupPoseBounds(),
backgroundColor: UIColor = .clear backgroundColor: UIColor = .clear
) { ) {
@ -204,8 +205,8 @@ public final class SpineUIView: MTKView {
@objc public convenience init( @objc public convenience init(
drawable: SkeletonDrawableWrapper, drawable: SkeletonDrawableWrapper,
controller: SpineController = SpineController(), controller: SpineController = SpineController(),
mode: Spine.ContentMode = .fit, mode: SpineContentMode = .fit,
alignment: Spine.Alignment = .center, alignment: SpineAlignment = .center,
boundsProvider: BoundsProvider = SetupPoseBounds(), boundsProvider: BoundsProvider = SetupPoseBounds(),
backgroundColor: UIColor = .clear backgroundColor: UIColor = .clear
) { ) {
@ -251,7 +252,7 @@ extension SpineUIView {
commandQueue: SpineObjects.shared.commandQueue, commandQueue: SpineObjects.shared.commandQueue,
pixelFormat: colorPixelFormat, pixelFormat: colorPixelFormat,
atlasPages: atlasPages, atlasPages: atlasPages,
pma: controller.drawable.atlas.isPma pma: false // TODO: Get PMA flag from atlas when API is available
) )
renderer?.delegate = controller renderer?.delegate = controller
renderer?.dataSource = controller renderer?.dataSource = controller

View File

@ -43,8 +43,8 @@ public struct SpineView: UIViewRepresentable {
private let source: SpineViewSource private let source: SpineViewSource
private let controller: SpineController private let controller: SpineController
private let mode: Spine.ContentMode private let mode: SpineContentMode
private let alignment: Spine.Alignment private let alignment: SpineAlignment
private let boundsProvider: BoundsProvider private let boundsProvider: BoundsProvider
private let backgroundColor: UIColor // Not using `SwiftUI.Color`, as briging to `UIColor` prior iOS 14 might not always work. private let backgroundColor: UIColor // Not using `SwiftUI.Color`, as briging to `UIColor` prior iOS 14 might not always work.
@ -71,8 +71,8 @@ public struct SpineView: UIViewRepresentable {
public init( public init(
from source: SpineViewSource, from source: SpineViewSource,
controller: SpineController = SpineController(), controller: SpineController = SpineController(),
mode: Spine.ContentMode = .fit, mode: SpineContentMode = .fit,
alignment: Spine.Alignment = .center, alignment: SpineAlignment = .center,
boundsProvider: BoundsProvider = SetupPoseBounds(), boundsProvider: BoundsProvider = SetupPoseBounds(),
backgroundColor: UIColor = .clear, backgroundColor: UIColor = .clear,
isRendering: Binding<Bool?> = .constant(nil) isRendering: Binding<Bool?> = .constant(nil)