Skip to content

Commit

Permalink
[State Management] Add bidirection state management for running state…
Browse files Browse the repository at this point in the history
… and transformation.
  • Loading branch information
li3zhen1 committed Feb 23, 2024
1 parent df0963e commit 2374701
Show file tree
Hide file tree
Showing 13 changed files with 173 additions and 132 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
B717595B2AFBFDBD000DF006 /* Lattice.swift in Sources */ = {isa = PBXBuildFile; fileRef = B717595A2AFBFDBD000DF006 /* Lattice.swift */; };
B762092F2B49FCD000476B93 /* MermaidVisualization.swift in Sources */ = {isa = PBXBuildFile; fileRef = B762092E2B49FCD000476B93 /* MermaidVisualization.swift */; };
B780DD7A2AF84ECB001C605F /* MyRing.swift in Sources */ = {isa = PBXBuildFile; fileRef = B780DD792AF84ECB001C605F /* MyRing.swift */; };
B79012AE2B88474F008F4C03 /* GraphStateToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = B79012AD2B88474F008F4C03 /* GraphStateToolbar.swift */; };
B7AFA55B2ADF4997009C7154 /* ForceDirectedGraphExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7AFA55A2ADF4997009C7154 /* ForceDirectedGraphExampleApp.swift */; };
B7AFA55D2ADF4997009C7154 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7AFA55C2ADF4997009C7154 /* ContentView.swift */; };
B7AFA55F2ADF4999009C7154 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B7AFA55E2ADF4999009C7154 /* Assets.xcassets */; };
Expand All @@ -26,6 +27,7 @@
B717595A2AFBFDBD000DF006 /* Lattice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lattice.swift; sourceTree = "<group>"; };
B762092E2B49FCD000476B93 /* MermaidVisualization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MermaidVisualization.swift; sourceTree = "<group>"; };
B780DD792AF84ECB001C605F /* MyRing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MyRing.swift; sourceTree = "<group>"; };
B79012AD2B88474F008F4C03 /* GraphStateToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphStateToolbar.swift; sourceTree = "<group>"; };
B7AFA5572ADF4997009C7154 /* ForceDirectedGraphExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ForceDirectedGraphExample.app; sourceTree = BUILT_PRODUCTS_DIR; };
B7AFA55A2ADF4997009C7154 /* ForceDirectedGraphExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForceDirectedGraphExampleApp.swift; sourceTree = "<group>"; };
B7AFA55C2ADF4997009C7154 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -78,6 +80,7 @@
B71759582AFBFC4B000DF006 /* Miserables.swift */,
B717595A2AFBFDBD000DF006 /* Lattice.swift */,
B762092E2B49FCD000476B93 /* MermaidVisualization.swift */,
B79012AD2B88474F008F4C03 /* GraphStateToolbar.swift */,
);
path = ForceDirectedGraphExample;
sourceTree = "<group>";
Expand Down Expand Up @@ -168,6 +171,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
B79012AE2B88474F008F4C03 /* GraphStateToolbar.swift in Sources */,
B717595B2AFBFDBD000DF006 /* Lattice.swift in Sources */,
B780DD7A2AF84ECB001C605F /* MyRing.swift in Sources */,
B7AFA55D2ADF4997009C7154 /* ContentView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ extension ExampleKind {

struct ContentView: View {

@State var selection: ExampleKind? = .classicMiserable
@State var selection: ExampleKind? = .ring

var body: some View {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// GraphStateToolbar.swift
// ForceDirectedGraphExample
//
// Created by li3zhen1 on 2/22/24.
//

import Foundation
import SwiftUI
import Grape

struct GraphStateToggle: View {
@Bindable var graphStates: ForceDirectedGraphState
var body: some View {

Group {
Button {
graphStates.modelTransform.scaling(by: 0.9)
} label: {
Image(systemName: "minus")
}
Text(String(format:"Scale: %.2f", graphStates.modelTransform.scale))
.fontDesign(.monospaced)
Button {
graphStates.modelTransform.scaling(by: 1.1)
} label: {
Image(systemName: "plus")
}
}

Button {
graphStates.isRunning.toggle()
} label: {
Image(systemName: graphStates.isRunning ? "pause.fill" : "play.fill")
Text(graphStates.isRunning ? "Pause" : "Start")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ struct Lattice: View {

let width = 20
let edge: [(Int, Int)]
@State var isRunning = false

@State var graphStates = ForceDirectedGraphState(
initialIsRunning: true
)

init() {
var edge = [(Int, Int)]()
Expand All @@ -32,7 +35,7 @@ struct Lattice: View {

@inlinable
var body: some View {
ForceDirectedGraph($isRunning) {
ForceDirectedGraph(states: graphStates) {

Series(0..<(width*width)) { i in
let _i = Double(i / width) / Double(width)
Expand All @@ -54,12 +57,7 @@ struct Lattice: View {
ManyBodyForce(strength: -0.8)
}
.toolbar {
Button {
isRunning = !isRunning
} label: {
Image(systemName: isRunning ? "pause.fill" : "play.fill")
Text(isRunning ? "Pause" : "Start")
}
GraphStateToggle(graphStates: graphStates)
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ struct MermaidVisualization: View {
tappedNode = $0
}
.ignoresSafeArea()
#if !os(xrOS)
#if !os(visionOS)
.inspector(isPresented: .constant(true)) {
VStack {
Text("Tapped: \(tappedNode ?? "nil")")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,26 @@ struct MiserableGraph: View {

private let graphData = getData(miserables)

@State private var isRunning = false
@State private var opacity: Double = 0.6
@State private var inspectorPresented = false

@State private var modelTransform: ViewportTransform = .identity.scale(by: 2.0)

@State private var stateMixin = ForceDirectedGraphState(initialIsRunning: false)
@State private var stateMixin = ForceDirectedGraphState(
initialIsRunning: false,
initialModelTransform: .identity.scale(by: 1.2)
)

@State private var opacity = 0.0

@ViewBuilder
func getLabel(_ text: String) -> some View {
Text(text)
.foregroundStyle(.background)
.font(.caption2)
.padding(.vertical, 2.0)
.padding(.horizontal, 6.0)
.background(alignment: .center) {
RoundedRectangle(cornerSize: .init(width: 12, height: 12))
.fill(.white)
.fill(.foreground)
.shadow(radius: 1.5, y: 1.0)
}
.padding()
Expand All @@ -40,30 +43,17 @@ struct MiserableGraph: View {
var body: some View {

ForceDirectedGraph(
$isRunning,
$modelTransform,
stateMixin: stateMixin
states: stateMixin
) {

Series(graphData.nodes) { node in
NodeMark(id: node.id)
.symbol(.asterisk)
.symbolSize(radius: 9.0)
.stroke()
// .foregroundStyle(
// colors[node.group % colors.count]
// .shadow(
// .inner(
// color: colors[node.group % colors.count].opacity(0.3),
// radius: 3,
// x: 0,
// y: 1.5
// )
// )
// )
// .richLabel(node.id, offset: .zero) {
// self.getLabel(node.id)
// }
.symbol(.circle)
.symbolSize(radius: 8.0)
.stroke()
.richLabel(node.id, offset: .zero) {
self.getLabel(node.id)
}
}

Series(graphData.links) { l in
Expand All @@ -78,9 +68,6 @@ struct MiserableGraph: View {
stiffness: .weightedByDegree(k: { _, _ in 1.0})
)
}
.onNodeTapped { node in
inspectorPresented = true
}
.opacity(opacity)
.animation(.easeInOut, value: opacity)

Expand All @@ -95,23 +82,29 @@ struct MiserableToolbarContent: View {
@Bindable var stateMixin: ForceDirectedGraphState
@Binding var opacity: Double

init(stateMixin: ForceDirectedGraphState, opacity: Binding<Double>) {
self.stateMixin = stateMixin
self._opacity = opacity
}

var body: some View {


Button {
stateMixin.modelTransform.scaling(by: 1.1)
} label: {
Text("\(stateMixin.modelTransform.scale)")
Group {
Button {
stateMixin.modelTransform.scaling(by: 1.1)
} label: {
Image(systemName: "minus")
}
Button {
stateMixin.modelTransform.scaling(by: 1.1)
} label: {
Text(String(format:"Scale: %.2f", stateMixin.modelTransform.scale))
.fontDesign(.monospaced)
}
Button {
stateMixin.modelTransform.scaling(by: 0.9)
} label: {
Image(systemName: "plus")
}
}


Button {
stateMixin.isRunning.toggle()
// print(stateMixin.isRunning)
if opacity < 1 {
opacity = 1
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,13 @@ import Grape
import SwiftUI
import ForceSimulation



struct MyRing: View {

@State var isRunning = false
@State var graphStates = ForceDirectedGraphState()

var body: some View {

ForceDirectedGraph($isRunning) {
ForceDirectedGraph(states: graphStates) {
Series(0..<20) { i in
NodeMark(id: 3 * i + 0)
.symbol(.circle)
Expand Down Expand Up @@ -51,12 +49,7 @@ struct MyRing: View {
CollideForce()
}
.toolbar {
Button {
isRunning = !isRunning
} label: {
Image(systemName: isRunning ? "pause.fill" : "play.fill")
Text(isRunning ? "Pause" : "Start")
}
GraphStateToggle(graphStates: graphStates)
}
}
}
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,18 @@ To use Grape in a [SwiftPM](https://swift.org/package-manager/) project, add thi

``` swift
dependencies: [
.package(url: "https://github.com/li3zhen1/Grape", from: "0.6.1")
.package(url: "https://github.com/li3zhen1/Grape", from: "0.7.0")
]
```

```swift
.product(name: "Grape", package: "Grape"),
```

> [!NOTE]
> The `Grape` module relies on [`Observation` framework](https://developer.apple.com/documentation/observation). For backdeployment, you may want to check some community shims like [`swift-perception`](https://github.com/pointfreeco/swift-perception).
> The `Grape` module may introduce breaking API changes in minor version changes before 1.0 release. The `ForceSimulation` module is stable in terms of public API now.
<br/>

<br/>
Expand All @@ -125,6 +129,7 @@ Grape ships 2 modules:
- The `Grape` module allows you to create force-directed graphs in SwiftUI Views.
- The `ForceSimulation` module is the underlying mechanism of `Grape`, and it helps you to create more complicated or customized force simulations. It also contains a `KDTree` data structure built with performance in mind, which can be useful for spatial partitioning tasks.


<br/>

### The `Grape` module
Expand All @@ -136,10 +141,13 @@ For detailed usage, please refer to [documentation](https://li3zhen1.github.io/G
import Grape

struct MyGraph: View {
@State var isRunning = true // start moving once appeared.

// States including running status, transformation, etc.
// Gives you a handle to control the states.
@State var graphStates = ForceDirectedGraphState()

var body: some View {
ForceDirectedGraph($isRunning) {
ForceDirectedGraph(states: graphStates) {

// Declare nodes and links like you would do in Swift Charts.
NodeMark(id: 0).foregroundStyle(.green)
Expand All @@ -160,6 +168,7 @@ struct MyGraph: View {
```



<br/>


Expand Down
1 change: 1 addition & 0 deletions Sources/ForceSimulation/Utils/SimulatableVector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ extension SIMD2: L2NormCalculatable where Scalar == Double {
}
}


extension SIMD3: L2NormCalculatable where Scalar == Float {
@inlinable
public func distanceSquared(to point: SIMD3<Scalar>) -> Scalar {
Expand Down
4 changes: 2 additions & 2 deletions Sources/Grape/Views/ForceDirectedGraph+Gesture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,10 @@ import SwiftUI
static var minimumScaleDelta: CGFloat { 0.001 }

@inlinable
static var minimumScale: CGFloat { 0.25 }
static var minimumScale: CGFloat { 1e-4 }

@inlinable
static var maximumScale: CGFloat { 4.0 }
static var maximumScale: CGFloat { .infinity }

@inlinable
static var magnificationDecay: CGFloat { 0.1 }
Expand Down
Loading

0 comments on commit 2374701

Please sign in to comment.