├── .gitignore ├── .swiftlint.yml ├── .travis.yml ├── DirectedGraph.xcodeproj ├── DirectedGraphTests_Info.plist ├── DirectedGraph_Info.plist ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ ├── WorkspaceSettings.xcsettings │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ ├── DirectedGraph-Package.xcscheme │ └── DirectedGraphDemo.xcscheme ├── DirectedGraphDemo ├── AppDelegate.swift ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Base.lproj │ └── LaunchScreen.storyboard ├── ContentView.swift ├── Info.plist ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── SceneDelegate.swift └── graph.json ├── LICENSE ├── Media └── Example1.png ├── Package.swift ├── README.md ├── Sources ├── Extensions │ ├── ArrayExtensions.swift │ ├── CGPointExtensions.swift │ ├── CGRectExtensions.swift │ ├── CGSizeExtensions.swift │ ├── CollectionExtensions.swift │ └── ComparableExtensions.swift ├── Graph │ ├── Edge.swift │ ├── Graph.swift │ ├── Node.swift │ ├── SimpleEdge.swift │ ├── SimpleGraph.swift │ └── SimpleNode.swift ├── Layouts │ ├── CircularLayoutEngine.swift │ ├── ForceDirectedLayoutEngine.swift │ ├── Layout.swift │ ├── LayoutEngine.swift │ ├── LayoutItem.swift │ └── RandomLayoutEngine.swift ├── Utilities │ ├── Palette.swift │ ├── Screen.swift │ └── SizeReader.swift └── Views │ ├── Arrow.swift │ ├── DefaultNodeView.swift │ ├── EdgeView.swift │ ├── EdgeViewModel.swift │ ├── GraphView.swift │ ├── GraphViewModel.swift │ ├── NodeView.swift │ ├── NodeViewModel.swift │ └── ScalableView.swift └── Tests ├── DirectedGraphTests └── XCTestManifests.swift ├── Extensions ├── ArrayExtensionsTests.swift ├── CGPointExtensionsTests.swift ├── CGRectExtensionsTests.swift ├── CGSizeExtensionsTests.swift ├── CollectionExtensionsTests.swift └── ComparableExtensionsTests.swift ├── Utilities └── PaletteTests.swift └── Views ├── EdgeViewModelTests.swift ├── EdgeViewTest.swift ├── GraphViewModelTests.swift ├── NodeViewModelTests.swift └── NodeViewTests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - trailing_whitespace 3 | - redundant_string_enum_value 4 | 5 | opt_in_rules: 6 | - empty_count 7 | - empty_string 8 | 9 | included: 10 | - Sources/ 11 | 12 | line_length: 13 | error: 250 14 | warning: 180 15 | ignores_comments: true 16 | ignores_function_declarations: true 17 | ignores_urls: true 18 | 19 | identifier_name: 20 | severity: warning 21 | min_length: 1 22 | max_length: 40 23 | excluded: # excluded via string array 24 | - id 25 | - URL 26 | validates_start_with_lowercase: true 27 | 28 | function_body_length: 29 | warning: 300 30 | error: 500 31 | 32 | function_parameter_count: 33 | warning: 6 34 | error: 8 35 | 36 | type_body_length: 37 | warning: 300 38 | error: 500 39 | # configurable rules can be customized from this configuration file 40 | # binary rules can set their severity level 41 | force_cast: warning # implicitly. Give warning only for force casting 42 | 43 | # or they can set both explicitly 44 | file_length: 45 | warning: 500 46 | error: 800 47 | ignore_comment_only_lines: true 48 | cyclomatic_complexity: 49 | warning: 15 50 | error: 25 51 | 52 | reporter: "xcode" 53 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | env: 2 | global: 3 | - CC_TEST_REPORTER_ID=ae07b8708fcc71d2b4f9d120e56641bae2b0f6ed1d78fb99160bc21bdd904cfd 4 | 5 | language: swift 6 | osx_image: xcode11.5 7 | install: 8 | - gem install xcpretty 9 | - gem install slather 10 | before_script: 11 | - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-darwin-amd64 > ./cc-test-reporter 12 | - chmod +x ./cc-test-reporter 13 | - ./cc-test-reporter before-build 14 | script: 15 | - xcodebuild test -enableCodeCoverage YES -project DirectedGraph.xcodeproj -scheme DirectedGraph-Package | xcpretty 16 | after_script: 17 | - slather coverage --ignore '../*' --input-format profdata -x --scheme DirectedGraph-Package DirectedGraph.xcodeproj 18 | - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT 19 | -------------------------------------------------------------------------------- /DirectedGraph.xcodeproj/DirectedGraphTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /DirectedGraph.xcodeproj/DirectedGraph_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /DirectedGraph.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 52; 7 | objects = { 8 | 9 | /* Begin PBXAggregateTarget section */ 10 | "DirectedGraph::DirectedGraphPackageTests::ProductTarget" /* DirectedGraphPackageTests */ = { 11 | isa = PBXAggregateTarget; 12 | buildConfigurationList = OBJ_102 /* Build configuration list for PBXAggregateTarget "DirectedGraphPackageTests" */; 13 | buildPhases = ( 14 | ); 15 | dependencies = ( 16 | OBJ_105 /* PBXTargetDependency */, 17 | ); 18 | name = DirectedGraphPackageTests; 19 | productName = DirectedGraphPackageTests; 20 | }; 21 | /* End PBXAggregateTarget section */ 22 | 23 | /* Begin PBXBuildFile section */ 24 | 03341E2C2486DE0400AAC9A8 /* graph.json in Resources */ = {isa = PBXBuildFile; fileRef = 03AF06CA2486DD71003ED12C /* graph.json */; }; 25 | 03341E312486FE7800AAC9A8 /* NodeViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03341E302486FE7800AAC9A8 /* NodeViewModelTests.swift */; }; 26 | 03341E332487003300AAC9A8 /* GraphViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03341E322487003300AAC9A8 /* GraphViewModelTests.swift */; }; 27 | 03341E382487066200AAC9A8 /* NodeViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03341E372487066100AAC9A8 /* NodeViewTests.swift */; }; 28 | 03341E3E248709EB00AAC9A8 /* ViewInspector in Frameworks */ = {isa = PBXBuildFile; productRef = 03341E3D248709EB00AAC9A8 /* ViewInspector */; }; 29 | 03341E402487829400AAC9A8 /* EdgeViewTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03341E3F2487829400AAC9A8 /* EdgeViewTest.swift */; }; 30 | 0358824D248F8E550084B52C /* EdgeViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0358824B248F8E460084B52C /* EdgeViewModelTests.swift */; }; 31 | 03AF06B32486DD45003ED12C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AF06B22486DD45003ED12C /* AppDelegate.swift */; }; 32 | 03AF06B52486DD45003ED12C /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AF06B42486DD45003ED12C /* SceneDelegate.swift */; }; 33 | 03AF06B92486DD47003ED12C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 03AF06B82486DD47003ED12C /* Assets.xcassets */; }; 34 | 03AF06BC2486DD47003ED12C /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 03AF06BB2486DD47003ED12C /* Preview Assets.xcassets */; }; 35 | 03AF06BF2486DD47003ED12C /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 03AF06BD2486DD47003ED12C /* LaunchScreen.storyboard */; }; 36 | 03AF06CC2486DD71003ED12C /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03AF06CB2486DD71003ED12C /* ContentView.swift */; }; 37 | 03C9E69B248E72F1007E449A /* CGSizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C9E69A248E72F1007E449A /* CGSizeExtensions.swift */; }; 38 | 03C9E69E248E7357007E449A /* CGRectExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03C9E69C248E732A007E449A /* CGRectExtensionsTests.swift */; }; 39 | 03C9E69F248E831F007E449A /* DirectedGraph.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "DirectedGraph::DirectedGraph::Product" /* DirectedGraph.framework */; }; 40 | 03C9E6A0248E831F007E449A /* DirectedGraph.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = "DirectedGraph::DirectedGraph::Product" /* DirectedGraph.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 41 | OBJ_100 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; }; 42 | OBJ_111 /* XCTestManifests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_44 /* XCTestManifests.swift */; }; 43 | OBJ_112 /* ArrayExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_46 /* ArrayExtensionsTests.swift */; }; 44 | OBJ_113 /* CGPointExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_47 /* CGPointExtensionsTests.swift */; }; 45 | OBJ_114 /* CGSizeExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_48 /* CGSizeExtensionsTests.swift */; }; 46 | OBJ_115 /* CollectionExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_49 /* CollectionExtensionsTests.swift */; }; 47 | OBJ_116 /* ComparableExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_50 /* ComparableExtensionsTests.swift */; }; 48 | OBJ_117 /* PaletteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_52 /* PaletteTests.swift */; }; 49 | OBJ_119 /* DirectedGraph.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "DirectedGraph::DirectedGraph::Product" /* DirectedGraph.framework */; }; 50 | OBJ_65 /* ArrayExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* ArrayExtensions.swift */; }; 51 | OBJ_66 /* CGPointExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* CGPointExtensions.swift */; }; 52 | OBJ_67 /* CGRectExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_11 /* CGRectExtensions.swift */; }; 53 | OBJ_68 /* CollectionExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_12 /* CollectionExtensions.swift */; }; 54 | OBJ_69 /* ComparableExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* ComparableExtensions.swift */; }; 55 | OBJ_70 /* Edge.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* Edge.swift */; }; 56 | OBJ_71 /* Graph.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_16 /* Graph.swift */; }; 57 | OBJ_72 /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_17 /* Node.swift */; }; 58 | OBJ_73 /* SimpleEdge.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_18 /* SimpleEdge.swift */; }; 59 | OBJ_74 /* SimpleGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_19 /* SimpleGraph.swift */; }; 60 | OBJ_75 /* SimpleNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_20 /* SimpleNode.swift */; }; 61 | OBJ_76 /* CircularLayoutEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_22 /* CircularLayoutEngine.swift */; }; 62 | OBJ_77 /* ForceDirectedLayoutEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_23 /* ForceDirectedLayoutEngine.swift */; }; 63 | OBJ_78 /* Layout.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_24 /* Layout.swift */; }; 64 | OBJ_79 /* LayoutEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_25 /* LayoutEngine.swift */; }; 65 | OBJ_80 /* LayoutItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_26 /* LayoutItem.swift */; }; 66 | OBJ_81 /* RandomLayoutEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_27 /* RandomLayoutEngine.swift */; }; 67 | OBJ_82 /* Palette.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_29 /* Palette.swift */; }; 68 | OBJ_83 /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_30 /* Screen.swift */; }; 69 | OBJ_84 /* SizeReader.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_31 /* SizeReader.swift */; }; 70 | OBJ_85 /* Arrow.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_33 /* Arrow.swift */; }; 71 | OBJ_86 /* DefaultNodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_34 /* DefaultNodeView.swift */; }; 72 | OBJ_87 /* EdgeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_35 /* EdgeView.swift */; }; 73 | OBJ_88 /* EdgeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_36 /* EdgeViewModel.swift */; }; 74 | OBJ_89 /* GraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_37 /* GraphView.swift */; }; 75 | OBJ_90 /* GraphViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_38 /* GraphViewModel.swift */; }; 76 | OBJ_91 /* NodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_39 /* NodeView.swift */; }; 77 | OBJ_92 /* NodeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_40 /* NodeViewModel.swift */; }; 78 | OBJ_93 /* ScalableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_41 /* ScalableView.swift */; }; 79 | /* End PBXBuildFile section */ 80 | 81 | /* Begin PBXContainerItemProxy section */ 82 | 03AF06AA2486D735003ED12C /* PBXContainerItemProxy */ = { 83 | isa = PBXContainerItemProxy; 84 | containerPortal = OBJ_1 /* Project object */; 85 | proxyType = 1; 86 | remoteGlobalIDString = "DirectedGraph::DirectedGraph"; 87 | remoteInfo = DirectedGraph; 88 | }; 89 | 03AF06AB2486D736003ED12C /* PBXContainerItemProxy */ = { 90 | isa = PBXContainerItemProxy; 91 | containerPortal = OBJ_1 /* Project object */; 92 | proxyType = 1; 93 | remoteGlobalIDString = "DirectedGraph::DirectedGraphTests"; 94 | remoteInfo = DirectedGraphTests; 95 | }; 96 | 03C9E6A1248E831F007E449A /* PBXContainerItemProxy */ = { 97 | isa = PBXContainerItemProxy; 98 | containerPortal = OBJ_1 /* Project object */; 99 | proxyType = 1; 100 | remoteGlobalIDString = "DirectedGraph::DirectedGraph"; 101 | remoteInfo = DirectedGraph; 102 | }; 103 | /* End PBXContainerItemProxy section */ 104 | 105 | /* Begin PBXCopyFilesBuildPhase section */ 106 | 03C9E6A3248E831F007E449A /* Embed Frameworks */ = { 107 | isa = PBXCopyFilesBuildPhase; 108 | buildActionMask = 2147483647; 109 | dstPath = ""; 110 | dstSubfolderSpec = 10; 111 | files = ( 112 | 03C9E6A0248E831F007E449A /* DirectedGraph.framework in Embed Frameworks */, 113 | ); 114 | name = "Embed Frameworks"; 115 | runOnlyForDeploymentPostprocessing = 0; 116 | }; 117 | /* End PBXCopyFilesBuildPhase section */ 118 | 119 | /* Begin PBXFileReference section */ 120 | 03341E2E2486EAD400AAC9A8 /* Example1.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = Example1.png; sourceTree = ""; }; 121 | 03341E302486FE7800AAC9A8 /* NodeViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeViewModelTests.swift; sourceTree = ""; }; 122 | 03341E322487003300AAC9A8 /* GraphViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphViewModelTests.swift; sourceTree = ""; }; 123 | 03341E372487066100AAC9A8 /* NodeViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeViewTests.swift; sourceTree = ""; }; 124 | 03341E3F2487829400AAC9A8 /* EdgeViewTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeViewTest.swift; sourceTree = ""; }; 125 | 0358824B248F8E460084B52C /* EdgeViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeViewModelTests.swift; sourceTree = ""; }; 126 | 03AF06B02486DD45003ED12C /* DirectedGraphDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DirectedGraphDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 127 | 03AF06B22486DD45003ED12C /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 128 | 03AF06B42486DD45003ED12C /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 129 | 03AF06B82486DD47003ED12C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 130 | 03AF06BB2486DD47003ED12C /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 131 | 03AF06BE2486DD47003ED12C /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 132 | 03AF06C02486DD47003ED12C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 133 | 03AF06CA2486DD71003ED12C /* graph.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = graph.json; sourceTree = ""; }; 134 | 03AF06CB2486DD71003ED12C /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 135 | 03C9E698248E3D31007E449A /* .swiftlint.yml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = ""; }; 136 | 03C9E69A248E72F1007E449A /* CGSizeExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGSizeExtensions.swift; sourceTree = ""; }; 137 | 03C9E69C248E732A007E449A /* CGRectExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGRectExtensionsTests.swift; sourceTree = ""; }; 138 | "DirectedGraph::DirectedGraph::Product" /* DirectedGraph.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = DirectedGraph.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 139 | "DirectedGraph::DirectedGraphTests::Product" /* DirectedGraphTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; path = DirectedGraphTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 140 | OBJ_10 /* CGPointExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGPointExtensions.swift; sourceTree = ""; }; 141 | OBJ_11 /* CGRectExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGRectExtensions.swift; sourceTree = ""; }; 142 | OBJ_12 /* CollectionExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtensions.swift; sourceTree = ""; }; 143 | OBJ_13 /* ComparableExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComparableExtensions.swift; sourceTree = ""; }; 144 | OBJ_15 /* Edge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Edge.swift; sourceTree = ""; }; 145 | OBJ_16 /* Graph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Graph.swift; sourceTree = ""; }; 146 | OBJ_17 /* Node.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = ""; }; 147 | OBJ_18 /* SimpleEdge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleEdge.swift; sourceTree = ""; }; 148 | OBJ_19 /* SimpleGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleGraph.swift; sourceTree = ""; }; 149 | OBJ_20 /* SimpleNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimpleNode.swift; sourceTree = ""; }; 150 | OBJ_22 /* CircularLayoutEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularLayoutEngine.swift; sourceTree = ""; }; 151 | OBJ_23 /* ForceDirectedLayoutEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForceDirectedLayoutEngine.swift; sourceTree = ""; }; 152 | OBJ_24 /* Layout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Layout.swift; sourceTree = ""; }; 153 | OBJ_25 /* LayoutEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutEngine.swift; sourceTree = ""; }; 154 | OBJ_26 /* LayoutItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutItem.swift; sourceTree = ""; }; 155 | OBJ_27 /* RandomLayoutEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RandomLayoutEngine.swift; sourceTree = ""; }; 156 | OBJ_29 /* Palette.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Palette.swift; sourceTree = ""; }; 157 | OBJ_30 /* Screen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Screen.swift; sourceTree = ""; }; 158 | OBJ_31 /* SizeReader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SizeReader.swift; sourceTree = ""; }; 159 | OBJ_33 /* Arrow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Arrow.swift; sourceTree = ""; }; 160 | OBJ_34 /* DefaultNodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultNodeView.swift; sourceTree = ""; }; 161 | OBJ_35 /* EdgeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeView.swift; sourceTree = ""; }; 162 | OBJ_36 /* EdgeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeViewModel.swift; sourceTree = ""; }; 163 | OBJ_37 /* GraphView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphView.swift; sourceTree = ""; }; 164 | OBJ_38 /* GraphViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphViewModel.swift; sourceTree = ""; }; 165 | OBJ_39 /* NodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeView.swift; sourceTree = ""; }; 166 | OBJ_40 /* NodeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NodeViewModel.swift; sourceTree = ""; }; 167 | OBJ_41 /* ScalableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScalableView.swift; sourceTree = ""; }; 168 | OBJ_44 /* XCTestManifests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCTestManifests.swift; sourceTree = ""; }; 169 | OBJ_46 /* ArrayExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayExtensionsTests.swift; sourceTree = ""; }; 170 | OBJ_47 /* CGPointExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGPointExtensionsTests.swift; sourceTree = ""; }; 171 | OBJ_48 /* CGSizeExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGSizeExtensionsTests.swift; sourceTree = ""; }; 172 | OBJ_49 /* CollectionExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionExtensionsTests.swift; sourceTree = ""; }; 173 | OBJ_50 /* ComparableExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComparableExtensionsTests.swift; sourceTree = ""; }; 174 | OBJ_52 /* PaletteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaletteTests.swift; sourceTree = ""; }; 175 | OBJ_57 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; 176 | OBJ_58 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 177 | OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 178 | OBJ_9 /* ArrayExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayExtensions.swift; sourceTree = ""; }; 179 | /* End PBXFileReference section */ 180 | 181 | /* Begin PBXFrameworksBuildPhase section */ 182 | 03AF06AD2486DD45003ED12C /* Frameworks */ = { 183 | isa = PBXFrameworksBuildPhase; 184 | buildActionMask = 2147483647; 185 | files = ( 186 | 03C9E69F248E831F007E449A /* DirectedGraph.framework in Frameworks */, 187 | ); 188 | runOnlyForDeploymentPostprocessing = 0; 189 | }; 190 | OBJ_118 /* Frameworks */ = { 191 | isa = PBXFrameworksBuildPhase; 192 | buildActionMask = 0; 193 | files = ( 194 | OBJ_119 /* DirectedGraph.framework in Frameworks */, 195 | 03341E3E248709EB00AAC9A8 /* ViewInspector in Frameworks */, 196 | ); 197 | runOnlyForDeploymentPostprocessing = 0; 198 | }; 199 | OBJ_94 /* Frameworks */ = { 200 | isa = PBXFrameworksBuildPhase; 201 | buildActionMask = 0; 202 | files = ( 203 | ); 204 | runOnlyForDeploymentPostprocessing = 0; 205 | }; 206 | /* End PBXFrameworksBuildPhase section */ 207 | 208 | /* Begin PBXGroup section */ 209 | 03341E2D2486EAD400AAC9A8 /* Media */ = { 210 | isa = PBXGroup; 211 | children = ( 212 | 03341E2E2486EAD400AAC9A8 /* Example1.png */, 213 | ); 214 | path = Media; 215 | sourceTree = ""; 216 | }; 217 | 03341E2F2486FE5600AAC9A8 /* Views */ = { 218 | isa = PBXGroup; 219 | children = ( 220 | 03341E302486FE7800AAC9A8 /* NodeViewModelTests.swift */, 221 | 03341E322487003300AAC9A8 /* GraphViewModelTests.swift */, 222 | 03341E372487066100AAC9A8 /* NodeViewTests.swift */, 223 | 03341E3F2487829400AAC9A8 /* EdgeViewTest.swift */, 224 | 0358824B248F8E460084B52C /* EdgeViewModelTests.swift */, 225 | ); 226 | path = Views; 227 | sourceTree = ""; 228 | }; 229 | 03AF06B12486DD45003ED12C /* DirectedGraphDemo */ = { 230 | isa = PBXGroup; 231 | children = ( 232 | 03AF06B22486DD45003ED12C /* AppDelegate.swift */, 233 | 03AF06CB2486DD71003ED12C /* ContentView.swift */, 234 | 03AF06B42486DD45003ED12C /* SceneDelegate.swift */, 235 | 03AF06CA2486DD71003ED12C /* graph.json */, 236 | 03AF06B82486DD47003ED12C /* Assets.xcassets */, 237 | 03AF06BD2486DD47003ED12C /* LaunchScreen.storyboard */, 238 | 03AF06C02486DD47003ED12C /* Info.plist */, 239 | 03AF06BA2486DD47003ED12C /* Preview Content */, 240 | ); 241 | path = DirectedGraphDemo; 242 | sourceTree = ""; 243 | }; 244 | 03AF06BA2486DD47003ED12C /* Preview Content */ = { 245 | isa = PBXGroup; 246 | children = ( 247 | 03AF06BB2486DD47003ED12C /* Preview Assets.xcassets */, 248 | ); 249 | path = "Preview Content"; 250 | sourceTree = ""; 251 | }; 252 | 03AF06C42486DD62003ED12C /* Frameworks */ = { 253 | isa = PBXGroup; 254 | children = ( 255 | ); 256 | name = Frameworks; 257 | sourceTree = ""; 258 | }; 259 | 03C9E699248E6989007E449A /* Metadata */ = { 260 | isa = PBXGroup; 261 | children = ( 262 | OBJ_58 /* README.md */, 263 | OBJ_57 /* LICENSE */, 264 | OBJ_6 /* Package.swift */, 265 | 03C9E698248E3D31007E449A /* .swiftlint.yml */, 266 | ); 267 | name = Metadata; 268 | sourceTree = ""; 269 | }; 270 | OBJ_14 /* Graph */ = { 271 | isa = PBXGroup; 272 | children = ( 273 | OBJ_15 /* Edge.swift */, 274 | OBJ_16 /* Graph.swift */, 275 | OBJ_17 /* Node.swift */, 276 | OBJ_18 /* SimpleEdge.swift */, 277 | OBJ_19 /* SimpleGraph.swift */, 278 | OBJ_20 /* SimpleNode.swift */, 279 | ); 280 | path = Graph; 281 | sourceTree = ""; 282 | }; 283 | OBJ_21 /* Layouts */ = { 284 | isa = PBXGroup; 285 | children = ( 286 | OBJ_22 /* CircularLayoutEngine.swift */, 287 | OBJ_23 /* ForceDirectedLayoutEngine.swift */, 288 | OBJ_24 /* Layout.swift */, 289 | OBJ_25 /* LayoutEngine.swift */, 290 | OBJ_26 /* LayoutItem.swift */, 291 | OBJ_27 /* RandomLayoutEngine.swift */, 292 | ); 293 | path = Layouts; 294 | sourceTree = ""; 295 | }; 296 | OBJ_28 /* Utilities */ = { 297 | isa = PBXGroup; 298 | children = ( 299 | OBJ_29 /* Palette.swift */, 300 | OBJ_30 /* Screen.swift */, 301 | OBJ_31 /* SizeReader.swift */, 302 | ); 303 | path = Utilities; 304 | sourceTree = ""; 305 | }; 306 | OBJ_32 /* Views */ = { 307 | isa = PBXGroup; 308 | children = ( 309 | OBJ_33 /* Arrow.swift */, 310 | OBJ_34 /* DefaultNodeView.swift */, 311 | OBJ_35 /* EdgeView.swift */, 312 | OBJ_36 /* EdgeViewModel.swift */, 313 | OBJ_37 /* GraphView.swift */, 314 | OBJ_38 /* GraphViewModel.swift */, 315 | OBJ_39 /* NodeView.swift */, 316 | OBJ_40 /* NodeViewModel.swift */, 317 | OBJ_41 /* ScalableView.swift */, 318 | ); 319 | path = Views; 320 | sourceTree = ""; 321 | }; 322 | OBJ_42 /* Tests */ = { 323 | isa = PBXGroup; 324 | children = ( 325 | OBJ_43 /* DirectedGraphTests */, 326 | OBJ_45 /* Extensions */, 327 | OBJ_51 /* Utilities */, 328 | 03341E2F2486FE5600AAC9A8 /* Views */, 329 | ); 330 | path = Tests; 331 | sourceTree = SOURCE_ROOT; 332 | }; 333 | OBJ_43 /* DirectedGraphTests */ = { 334 | isa = PBXGroup; 335 | children = ( 336 | OBJ_44 /* XCTestManifests.swift */, 337 | ); 338 | path = DirectedGraphTests; 339 | sourceTree = ""; 340 | }; 341 | OBJ_45 /* Extensions */ = { 342 | isa = PBXGroup; 343 | children = ( 344 | OBJ_46 /* ArrayExtensionsTests.swift */, 345 | OBJ_47 /* CGPointExtensionsTests.swift */, 346 | 03C9E69C248E732A007E449A /* CGRectExtensionsTests.swift */, 347 | OBJ_48 /* CGSizeExtensionsTests.swift */, 348 | OBJ_49 /* CollectionExtensionsTests.swift */, 349 | OBJ_50 /* ComparableExtensionsTests.swift */, 350 | ); 351 | path = Extensions; 352 | sourceTree = ""; 353 | }; 354 | OBJ_5 = { 355 | isa = PBXGroup; 356 | children = ( 357 | 03C9E699248E6989007E449A /* Metadata */, 358 | OBJ_7 /* Sources */, 359 | OBJ_42 /* Tests */, 360 | 03341E2D2486EAD400AAC9A8 /* Media */, 361 | 03AF06B12486DD45003ED12C /* DirectedGraphDemo */, 362 | OBJ_53 /* Products */, 363 | 03AF06C42486DD62003ED12C /* Frameworks */, 364 | ); 365 | sourceTree = ""; 366 | }; 367 | OBJ_51 /* Utilities */ = { 368 | isa = PBXGroup; 369 | children = ( 370 | OBJ_52 /* PaletteTests.swift */, 371 | ); 372 | path = Utilities; 373 | sourceTree = ""; 374 | }; 375 | OBJ_53 /* Products */ = { 376 | isa = PBXGroup; 377 | children = ( 378 | "DirectedGraph::DirectedGraphTests::Product" /* DirectedGraphTests.xctest */, 379 | "DirectedGraph::DirectedGraph::Product" /* DirectedGraph.framework */, 380 | 03AF06B02486DD45003ED12C /* DirectedGraphDemo.app */, 381 | ); 382 | name = Products; 383 | sourceTree = BUILT_PRODUCTS_DIR; 384 | }; 385 | OBJ_7 /* Sources */ = { 386 | isa = PBXGroup; 387 | children = ( 388 | OBJ_8 /* Extensions */, 389 | OBJ_14 /* Graph */, 390 | OBJ_21 /* Layouts */, 391 | OBJ_28 /* Utilities */, 392 | OBJ_32 /* Views */, 393 | ); 394 | path = Sources; 395 | sourceTree = SOURCE_ROOT; 396 | }; 397 | OBJ_8 /* Extensions */ = { 398 | isa = PBXGroup; 399 | children = ( 400 | OBJ_9 /* ArrayExtensions.swift */, 401 | OBJ_10 /* CGPointExtensions.swift */, 402 | OBJ_11 /* CGRectExtensions.swift */, 403 | 03C9E69A248E72F1007E449A /* CGSizeExtensions.swift */, 404 | OBJ_12 /* CollectionExtensions.swift */, 405 | OBJ_13 /* ComparableExtensions.swift */, 406 | ); 407 | path = Extensions; 408 | sourceTree = ""; 409 | }; 410 | /* End PBXGroup section */ 411 | 412 | /* Begin PBXNativeTarget section */ 413 | 03AF06AF2486DD45003ED12C /* DirectedGraphDemo */ = { 414 | isa = PBXNativeTarget; 415 | buildConfigurationList = 03AF06C32486DD47003ED12C /* Build configuration list for PBXNativeTarget "DirectedGraphDemo" */; 416 | buildPhases = ( 417 | 03AF06AC2486DD45003ED12C /* Sources */, 418 | 03AF06AD2486DD45003ED12C /* Frameworks */, 419 | 03AF06AE2486DD45003ED12C /* Resources */, 420 | 03C9E6A3248E831F007E449A /* Embed Frameworks */, 421 | ); 422 | buildRules = ( 423 | ); 424 | dependencies = ( 425 | 03C9E6A2248E831F007E449A /* PBXTargetDependency */, 426 | ); 427 | name = DirectedGraphDemo; 428 | productName = DirectedGraphDemo; 429 | productReference = 03AF06B02486DD45003ED12C /* DirectedGraphDemo.app */; 430 | productType = "com.apple.product-type.application"; 431 | }; 432 | "DirectedGraph::DirectedGraph" /* DirectedGraph */ = { 433 | isa = PBXNativeTarget; 434 | buildConfigurationList = OBJ_61 /* Build configuration list for PBXNativeTarget "DirectedGraph" */; 435 | buildPhases = ( 436 | OBJ_64 /* Sources */, 437 | OBJ_94 /* Frameworks */, 438 | 03C9E697248E3D09007E449A /* SwiftLint */, 439 | ); 440 | buildRules = ( 441 | ); 442 | dependencies = ( 443 | ); 444 | name = DirectedGraph; 445 | packageProductDependencies = ( 446 | ); 447 | productName = DirectedGraph; 448 | productReference = "DirectedGraph::DirectedGraph::Product" /* DirectedGraph.framework */; 449 | productType = "com.apple.product-type.framework"; 450 | }; 451 | "DirectedGraph::DirectedGraphTests" /* DirectedGraphTests */ = { 452 | isa = PBXNativeTarget; 453 | buildConfigurationList = OBJ_107 /* Build configuration list for PBXNativeTarget "DirectedGraphTests" */; 454 | buildPhases = ( 455 | OBJ_110 /* Sources */, 456 | OBJ_118 /* Frameworks */, 457 | ); 458 | buildRules = ( 459 | ); 460 | dependencies = ( 461 | OBJ_120 /* PBXTargetDependency */, 462 | ); 463 | name = DirectedGraphTests; 464 | packageProductDependencies = ( 465 | 03341E3D248709EB00AAC9A8 /* ViewInspector */, 466 | ); 467 | productName = DirectedGraphTests; 468 | productReference = "DirectedGraph::DirectedGraphTests::Product" /* DirectedGraphTests.xctest */; 469 | productType = "com.apple.product-type.bundle.unit-test"; 470 | }; 471 | "DirectedGraph::SwiftPMPackageDescription" /* DirectedGraphPackageDescription */ = { 472 | isa = PBXNativeTarget; 473 | buildConfigurationList = OBJ_96 /* Build configuration list for PBXNativeTarget "DirectedGraphPackageDescription" */; 474 | buildPhases = ( 475 | OBJ_99 /* Sources */, 476 | ); 477 | buildRules = ( 478 | ); 479 | dependencies = ( 480 | ); 481 | name = DirectedGraphPackageDescription; 482 | productName = DirectedGraphPackageDescription; 483 | productType = "com.apple.product-type.framework"; 484 | }; 485 | /* End PBXNativeTarget section */ 486 | 487 | /* Begin PBXProject section */ 488 | OBJ_1 /* Project object */ = { 489 | isa = PBXProject; 490 | attributes = { 491 | LastSwiftMigration = 9999; 492 | LastSwiftUpdateCheck = 1150; 493 | LastUpgradeCheck = 1150; 494 | TargetAttributes = { 495 | 03AF06AF2486DD45003ED12C = { 496 | CreatedOnToolsVersion = 11.5; 497 | ProvisioningStyle = Automatic; 498 | }; 499 | }; 500 | }; 501 | buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "DirectedGraph" */; 502 | compatibilityVersion = "Xcode 3.2"; 503 | developmentRegion = en; 504 | hasScannedForEncodings = 0; 505 | knownRegions = ( 506 | en, 507 | Base, 508 | ); 509 | mainGroup = OBJ_5; 510 | packageReferences = ( 511 | 03341E3C248709EB00AAC9A8 /* XCRemoteSwiftPackageReference "ViewInspector" */, 512 | ); 513 | productRefGroup = OBJ_53 /* Products */; 514 | projectDirPath = ""; 515 | projectRoot = ""; 516 | targets = ( 517 | "DirectedGraph::DirectedGraph" /* DirectedGraph */, 518 | "DirectedGraph::SwiftPMPackageDescription" /* DirectedGraphPackageDescription */, 519 | "DirectedGraph::DirectedGraphPackageTests::ProductTarget" /* DirectedGraphPackageTests */, 520 | "DirectedGraph::DirectedGraphTests" /* DirectedGraphTests */, 521 | 03AF06AF2486DD45003ED12C /* DirectedGraphDemo */, 522 | ); 523 | }; 524 | /* End PBXProject section */ 525 | 526 | /* Begin PBXResourcesBuildPhase section */ 527 | 03AF06AE2486DD45003ED12C /* Resources */ = { 528 | isa = PBXResourcesBuildPhase; 529 | buildActionMask = 2147483647; 530 | files = ( 531 | 03341E2C2486DE0400AAC9A8 /* graph.json in Resources */, 532 | 03AF06BF2486DD47003ED12C /* LaunchScreen.storyboard in Resources */, 533 | 03AF06BC2486DD47003ED12C /* Preview Assets.xcassets in Resources */, 534 | 03AF06B92486DD47003ED12C /* Assets.xcassets in Resources */, 535 | ); 536 | runOnlyForDeploymentPostprocessing = 0; 537 | }; 538 | /* End PBXResourcesBuildPhase section */ 539 | 540 | /* Begin PBXShellScriptBuildPhase section */ 541 | 03C9E697248E3D09007E449A /* SwiftLint */ = { 542 | isa = PBXShellScriptBuildPhase; 543 | buildActionMask = 2147483647; 544 | files = ( 545 | ); 546 | inputFileListPaths = ( 547 | ); 548 | inputPaths = ( 549 | ); 550 | name = SwiftLint; 551 | outputFileListPaths = ( 552 | ); 553 | outputPaths = ( 554 | ); 555 | runOnlyForDeploymentPostprocessing = 0; 556 | shellPath = /bin/sh; 557 | shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n"; 558 | }; 559 | /* End PBXShellScriptBuildPhase section */ 560 | 561 | /* Begin PBXSourcesBuildPhase section */ 562 | 03AF06AC2486DD45003ED12C /* Sources */ = { 563 | isa = PBXSourcesBuildPhase; 564 | buildActionMask = 2147483647; 565 | files = ( 566 | 03AF06B32486DD45003ED12C /* AppDelegate.swift in Sources */, 567 | 03AF06B52486DD45003ED12C /* SceneDelegate.swift in Sources */, 568 | 03AF06CC2486DD71003ED12C /* ContentView.swift in Sources */, 569 | ); 570 | runOnlyForDeploymentPostprocessing = 0; 571 | }; 572 | OBJ_110 /* Sources */ = { 573 | isa = PBXSourcesBuildPhase; 574 | buildActionMask = 0; 575 | files = ( 576 | 03341E312486FE7800AAC9A8 /* NodeViewModelTests.swift in Sources */, 577 | OBJ_111 /* XCTestManifests.swift in Sources */, 578 | OBJ_112 /* ArrayExtensionsTests.swift in Sources */, 579 | 03341E332487003300AAC9A8 /* GraphViewModelTests.swift in Sources */, 580 | OBJ_113 /* CGPointExtensionsTests.swift in Sources */, 581 | 03341E402487829400AAC9A8 /* EdgeViewTest.swift in Sources */, 582 | OBJ_114 /* CGSizeExtensionsTests.swift in Sources */, 583 | 0358824D248F8E550084B52C /* EdgeViewModelTests.swift in Sources */, 584 | OBJ_115 /* CollectionExtensionsTests.swift in Sources */, 585 | OBJ_116 /* ComparableExtensionsTests.swift in Sources */, 586 | 03C9E69E248E7357007E449A /* CGRectExtensionsTests.swift in Sources */, 587 | 03341E382487066200AAC9A8 /* NodeViewTests.swift in Sources */, 588 | OBJ_117 /* PaletteTests.swift in Sources */, 589 | ); 590 | runOnlyForDeploymentPostprocessing = 0; 591 | }; 592 | OBJ_64 /* Sources */ = { 593 | isa = PBXSourcesBuildPhase; 594 | buildActionMask = 0; 595 | files = ( 596 | OBJ_65 /* ArrayExtensions.swift in Sources */, 597 | OBJ_66 /* CGPointExtensions.swift in Sources */, 598 | OBJ_67 /* CGRectExtensions.swift in Sources */, 599 | OBJ_68 /* CollectionExtensions.swift in Sources */, 600 | OBJ_69 /* ComparableExtensions.swift in Sources */, 601 | OBJ_70 /* Edge.swift in Sources */, 602 | OBJ_71 /* Graph.swift in Sources */, 603 | OBJ_72 /* Node.swift in Sources */, 604 | OBJ_73 /* SimpleEdge.swift in Sources */, 605 | OBJ_74 /* SimpleGraph.swift in Sources */, 606 | 03C9E69B248E72F1007E449A /* CGSizeExtensions.swift in Sources */, 607 | OBJ_75 /* SimpleNode.swift in Sources */, 608 | OBJ_76 /* CircularLayoutEngine.swift in Sources */, 609 | OBJ_77 /* ForceDirectedLayoutEngine.swift in Sources */, 610 | OBJ_78 /* Layout.swift in Sources */, 611 | OBJ_79 /* LayoutEngine.swift in Sources */, 612 | OBJ_80 /* LayoutItem.swift in Sources */, 613 | OBJ_81 /* RandomLayoutEngine.swift in Sources */, 614 | OBJ_82 /* Palette.swift in Sources */, 615 | OBJ_83 /* Screen.swift in Sources */, 616 | OBJ_84 /* SizeReader.swift in Sources */, 617 | OBJ_85 /* Arrow.swift in Sources */, 618 | OBJ_86 /* DefaultNodeView.swift in Sources */, 619 | OBJ_87 /* EdgeView.swift in Sources */, 620 | OBJ_88 /* EdgeViewModel.swift in Sources */, 621 | OBJ_89 /* GraphView.swift in Sources */, 622 | OBJ_90 /* GraphViewModel.swift in Sources */, 623 | OBJ_91 /* NodeView.swift in Sources */, 624 | OBJ_92 /* NodeViewModel.swift in Sources */, 625 | OBJ_93 /* ScalableView.swift in Sources */, 626 | ); 627 | runOnlyForDeploymentPostprocessing = 0; 628 | }; 629 | OBJ_99 /* Sources */ = { 630 | isa = PBXSourcesBuildPhase; 631 | buildActionMask = 0; 632 | files = ( 633 | OBJ_100 /* Package.swift in Sources */, 634 | ); 635 | runOnlyForDeploymentPostprocessing = 0; 636 | }; 637 | /* End PBXSourcesBuildPhase section */ 638 | 639 | /* Begin PBXTargetDependency section */ 640 | 03C9E6A2248E831F007E449A /* PBXTargetDependency */ = { 641 | isa = PBXTargetDependency; 642 | target = "DirectedGraph::DirectedGraph" /* DirectedGraph */; 643 | targetProxy = 03C9E6A1248E831F007E449A /* PBXContainerItemProxy */; 644 | }; 645 | OBJ_105 /* PBXTargetDependency */ = { 646 | isa = PBXTargetDependency; 647 | target = "DirectedGraph::DirectedGraphTests" /* DirectedGraphTests */; 648 | targetProxy = 03AF06AB2486D736003ED12C /* PBXContainerItemProxy */; 649 | }; 650 | OBJ_120 /* PBXTargetDependency */ = { 651 | isa = PBXTargetDependency; 652 | target = "DirectedGraph::DirectedGraph" /* DirectedGraph */; 653 | targetProxy = 03AF06AA2486D735003ED12C /* PBXContainerItemProxy */; 654 | }; 655 | /* End PBXTargetDependency section */ 656 | 657 | /* Begin PBXVariantGroup section */ 658 | 03AF06BD2486DD47003ED12C /* LaunchScreen.storyboard */ = { 659 | isa = PBXVariantGroup; 660 | children = ( 661 | 03AF06BE2486DD47003ED12C /* Base */, 662 | ); 663 | name = LaunchScreen.storyboard; 664 | sourceTree = ""; 665 | }; 666 | /* End PBXVariantGroup section */ 667 | 668 | /* Begin XCBuildConfiguration section */ 669 | 03AF06C12486DD47003ED12C /* Debug */ = { 670 | isa = XCBuildConfiguration; 671 | buildSettings = { 672 | ALWAYS_SEARCH_USER_PATHS = NO; 673 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 674 | CLANG_ANALYZER_NONNULL = YES; 675 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 676 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 677 | CLANG_CXX_LIBRARY = "libc++"; 678 | CLANG_ENABLE_MODULES = YES; 679 | CLANG_ENABLE_OBJC_WEAK = YES; 680 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 681 | CLANG_WARN_BOOL_CONVERSION = YES; 682 | CLANG_WARN_COMMA = YES; 683 | CLANG_WARN_CONSTANT_CONVERSION = YES; 684 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 685 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 686 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 687 | CLANG_WARN_EMPTY_BODY = YES; 688 | CLANG_WARN_ENUM_CONVERSION = YES; 689 | CLANG_WARN_INFINITE_RECURSION = YES; 690 | CLANG_WARN_INT_CONVERSION = YES; 691 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 692 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 693 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 694 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 695 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 696 | CLANG_WARN_STRICT_PROTOTYPES = YES; 697 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 698 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 699 | CLANG_WARN_UNREACHABLE_CODE = YES; 700 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 701 | CODE_SIGN_STYLE = Automatic; 702 | DEVELOPMENT_ASSET_PATHS = "\"DirectedGraphDemo/Preview Content\""; 703 | ENABLE_PREVIEWS = YES; 704 | ENABLE_STRICT_OBJC_MSGSEND = YES; 705 | ENABLE_TESTABILITY = YES; 706 | GCC_C_LANGUAGE_STANDARD = gnu11; 707 | GCC_DYNAMIC_NO_PIC = NO; 708 | GCC_NO_COMMON_BLOCKS = YES; 709 | GCC_PREPROCESSOR_DEFINITIONS = ( 710 | "DEBUG=1", 711 | "$(inherited)", 712 | ); 713 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 714 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 715 | GCC_WARN_UNDECLARED_SELECTOR = YES; 716 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 717 | GCC_WARN_UNUSED_FUNCTION = YES; 718 | GCC_WARN_UNUSED_VARIABLE = YES; 719 | INFOPLIST_FILE = DirectedGraphDemo/Info.plist; 720 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 721 | LD_RUNPATH_SEARCH_PATHS = ( 722 | "$(inherited)", 723 | "@executable_path/Frameworks", 724 | ); 725 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 726 | MTL_FAST_MATH = YES; 727 | PRODUCT_BUNDLE_IDENTIFIER = com.nmandica.DirectedGraphDemo; 728 | PRODUCT_NAME = "$(TARGET_NAME)"; 729 | SDKROOT = iphoneos; 730 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 731 | SWIFT_VERSION = 5.0; 732 | TARGETED_DEVICE_FAMILY = "1,2"; 733 | }; 734 | name = Debug; 735 | }; 736 | 03AF06C22486DD47003ED12C /* Release */ = { 737 | isa = XCBuildConfiguration; 738 | buildSettings = { 739 | ALWAYS_SEARCH_USER_PATHS = NO; 740 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 741 | CLANG_ANALYZER_NONNULL = YES; 742 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 743 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 744 | CLANG_CXX_LIBRARY = "libc++"; 745 | CLANG_ENABLE_MODULES = YES; 746 | CLANG_ENABLE_OBJC_WEAK = YES; 747 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 748 | CLANG_WARN_BOOL_CONVERSION = YES; 749 | CLANG_WARN_COMMA = YES; 750 | CLANG_WARN_CONSTANT_CONVERSION = YES; 751 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 752 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 753 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 754 | CLANG_WARN_EMPTY_BODY = YES; 755 | CLANG_WARN_ENUM_CONVERSION = YES; 756 | CLANG_WARN_INFINITE_RECURSION = YES; 757 | CLANG_WARN_INT_CONVERSION = YES; 758 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 759 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 760 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 761 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 762 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 763 | CLANG_WARN_STRICT_PROTOTYPES = YES; 764 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 765 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 766 | CLANG_WARN_UNREACHABLE_CODE = YES; 767 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 768 | CODE_SIGN_STYLE = Automatic; 769 | COPY_PHASE_STRIP = NO; 770 | DEVELOPMENT_ASSET_PATHS = "\"DirectedGraphDemo/Preview Content\""; 771 | ENABLE_NS_ASSERTIONS = NO; 772 | ENABLE_PREVIEWS = YES; 773 | ENABLE_STRICT_OBJC_MSGSEND = YES; 774 | GCC_C_LANGUAGE_STANDARD = gnu11; 775 | GCC_NO_COMMON_BLOCKS = YES; 776 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 777 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 778 | GCC_WARN_UNDECLARED_SELECTOR = YES; 779 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 780 | GCC_WARN_UNUSED_FUNCTION = YES; 781 | GCC_WARN_UNUSED_VARIABLE = YES; 782 | INFOPLIST_FILE = DirectedGraphDemo/Info.plist; 783 | IPHONEOS_DEPLOYMENT_TARGET = 13.5; 784 | LD_RUNPATH_SEARCH_PATHS = ( 785 | "$(inherited)", 786 | "@executable_path/Frameworks", 787 | ); 788 | MTL_ENABLE_DEBUG_INFO = NO; 789 | MTL_FAST_MATH = YES; 790 | PRODUCT_BUNDLE_IDENTIFIER = com.nmandica.DirectedGraphDemo; 791 | PRODUCT_NAME = "$(TARGET_NAME)"; 792 | SDKROOT = iphoneos; 793 | SWIFT_VERSION = 5.0; 794 | TARGETED_DEVICE_FAMILY = "1,2"; 795 | VALIDATE_PRODUCT = YES; 796 | }; 797 | name = Release; 798 | }; 799 | OBJ_103 /* Debug */ = { 800 | isa = XCBuildConfiguration; 801 | buildSettings = { 802 | }; 803 | name = Debug; 804 | }; 805 | OBJ_104 /* Release */ = { 806 | isa = XCBuildConfiguration; 807 | buildSettings = { 808 | }; 809 | name = Release; 810 | }; 811 | OBJ_108 /* Debug */ = { 812 | isa = XCBuildConfiguration; 813 | buildSettings = { 814 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 815 | CLANG_ENABLE_MODULES = YES; 816 | FRAMEWORK_SEARCH_PATHS = ( 817 | "$(inherited)", 818 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 819 | ); 820 | HEADER_SEARCH_PATHS = "$(inherited)"; 821 | INFOPLIST_FILE = DirectedGraph.xcodeproj/DirectedGraphTests_Info.plist; 822 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 823 | LD_RUNPATH_SEARCH_PATHS = ( 824 | "$(inherited)", 825 | "@loader_path/../Frameworks", 826 | "@loader_path/Frameworks", 827 | ); 828 | MACOSX_DEPLOYMENT_TARGET = 10.15; 829 | OTHER_CFLAGS = "$(inherited)"; 830 | OTHER_LDFLAGS = "$(inherited)"; 831 | OTHER_SWIFT_FLAGS = "$(inherited)"; 832 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 833 | SWIFT_VERSION = 5.0; 834 | TARGET_NAME = DirectedGraphTests; 835 | TVOS_DEPLOYMENT_TARGET = 9.0; 836 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 837 | }; 838 | name = Debug; 839 | }; 840 | OBJ_109 /* Release */ = { 841 | isa = XCBuildConfiguration; 842 | buildSettings = { 843 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 844 | CLANG_ENABLE_MODULES = YES; 845 | FRAMEWORK_SEARCH_PATHS = ( 846 | "$(inherited)", 847 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 848 | ); 849 | HEADER_SEARCH_PATHS = "$(inherited)"; 850 | INFOPLIST_FILE = DirectedGraph.xcodeproj/DirectedGraphTests_Info.plist; 851 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 852 | LD_RUNPATH_SEARCH_PATHS = ( 853 | "$(inherited)", 854 | "@loader_path/../Frameworks", 855 | "@loader_path/Frameworks", 856 | ); 857 | MACOSX_DEPLOYMENT_TARGET = 10.15; 858 | OTHER_CFLAGS = "$(inherited)"; 859 | OTHER_LDFLAGS = "$(inherited)"; 860 | OTHER_SWIFT_FLAGS = "$(inherited)"; 861 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 862 | SWIFT_VERSION = 5.0; 863 | TARGET_NAME = DirectedGraphTests; 864 | TVOS_DEPLOYMENT_TARGET = 9.0; 865 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 866 | }; 867 | name = Release; 868 | }; 869 | OBJ_3 /* Debug */ = { 870 | isa = XCBuildConfiguration; 871 | buildSettings = { 872 | CLANG_ENABLE_OBJC_ARC = YES; 873 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 874 | CLANG_WARN_BOOL_CONVERSION = YES; 875 | CLANG_WARN_COMMA = YES; 876 | CLANG_WARN_CONSTANT_CONVERSION = YES; 877 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 878 | CLANG_WARN_EMPTY_BODY = YES; 879 | CLANG_WARN_ENUM_CONVERSION = YES; 880 | CLANG_WARN_INFINITE_RECURSION = YES; 881 | CLANG_WARN_INT_CONVERSION = YES; 882 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 883 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 884 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 885 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 886 | CLANG_WARN_STRICT_PROTOTYPES = YES; 887 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 888 | CLANG_WARN_UNREACHABLE_CODE = YES; 889 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 890 | COMBINE_HIDPI_IMAGES = YES; 891 | COPY_PHASE_STRIP = NO; 892 | DEBUG_INFORMATION_FORMAT = dwarf; 893 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 894 | ENABLE_NS_ASSERTIONS = YES; 895 | ENABLE_STRICT_OBJC_MSGSEND = YES; 896 | ENABLE_TESTABILITY = YES; 897 | GCC_NO_COMMON_BLOCKS = YES; 898 | GCC_OPTIMIZATION_LEVEL = 0; 899 | GCC_PREPROCESSOR_DEFINITIONS = ( 900 | "$(inherited)", 901 | "SWIFT_PACKAGE=1", 902 | "DEBUG=1", 903 | ); 904 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 905 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 906 | GCC_WARN_UNDECLARED_SELECTOR = YES; 907 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 908 | GCC_WARN_UNUSED_FUNCTION = YES; 909 | GCC_WARN_UNUSED_VARIABLE = YES; 910 | MACOSX_DEPLOYMENT_TARGET = 10.10; 911 | ONLY_ACTIVE_ARCH = YES; 912 | OTHER_SWIFT_FLAGS = "$(inherited) -DXcode"; 913 | PRODUCT_NAME = "$(TARGET_NAME)"; 914 | SDKROOT = macosx; 915 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 916 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE DEBUG"; 917 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 918 | USE_HEADERMAP = NO; 919 | }; 920 | name = Debug; 921 | }; 922 | OBJ_4 /* Release */ = { 923 | isa = XCBuildConfiguration; 924 | buildSettings = { 925 | CLANG_ENABLE_OBJC_ARC = YES; 926 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 927 | CLANG_WARN_BOOL_CONVERSION = YES; 928 | CLANG_WARN_COMMA = YES; 929 | CLANG_WARN_CONSTANT_CONVERSION = YES; 930 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 931 | CLANG_WARN_EMPTY_BODY = YES; 932 | CLANG_WARN_ENUM_CONVERSION = YES; 933 | CLANG_WARN_INFINITE_RECURSION = YES; 934 | CLANG_WARN_INT_CONVERSION = YES; 935 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 936 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 937 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 938 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 939 | CLANG_WARN_STRICT_PROTOTYPES = YES; 940 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 941 | CLANG_WARN_UNREACHABLE_CODE = YES; 942 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 943 | COMBINE_HIDPI_IMAGES = YES; 944 | COPY_PHASE_STRIP = YES; 945 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 946 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 947 | ENABLE_STRICT_OBJC_MSGSEND = YES; 948 | GCC_NO_COMMON_BLOCKS = YES; 949 | GCC_OPTIMIZATION_LEVEL = s; 950 | GCC_PREPROCESSOR_DEFINITIONS = ( 951 | "$(inherited)", 952 | "SWIFT_PACKAGE=1", 953 | ); 954 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 955 | GCC_WARN_ABOUT_RETURN_TYPE = YES; 956 | GCC_WARN_UNDECLARED_SELECTOR = YES; 957 | GCC_WARN_UNINITIALIZED_AUTOS = YES; 958 | GCC_WARN_UNUSED_FUNCTION = YES; 959 | GCC_WARN_UNUSED_VARIABLE = YES; 960 | MACOSX_DEPLOYMENT_TARGET = 10.10; 961 | OTHER_SWIFT_FLAGS = "$(inherited) -DXcode"; 962 | PRODUCT_NAME = "$(TARGET_NAME)"; 963 | SDKROOT = macosx; 964 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 965 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE"; 966 | SWIFT_COMPILATION_MODE = wholemodule; 967 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 968 | USE_HEADERMAP = NO; 969 | }; 970 | name = Release; 971 | }; 972 | OBJ_62 /* Debug */ = { 973 | isa = XCBuildConfiguration; 974 | buildSettings = { 975 | ENABLE_TESTABILITY = YES; 976 | FRAMEWORK_SEARCH_PATHS = ( 977 | "$(inherited)", 978 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 979 | ); 980 | HEADER_SEARCH_PATHS = "$(inherited)"; 981 | INFOPLIST_FILE = DirectedGraph.xcodeproj/DirectedGraph_Info.plist; 982 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 983 | LD_RUNPATH_SEARCH_PATHS = ( 984 | "$(inherited)", 985 | "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx", 986 | ); 987 | MACOSX_DEPLOYMENT_TARGET = 10.15; 988 | OTHER_CFLAGS = "$(inherited)"; 989 | OTHER_LDFLAGS = "$(inherited)"; 990 | OTHER_SWIFT_FLAGS = "$(inherited)"; 991 | PRODUCT_BUNDLE_IDENTIFIER = DirectedGraph; 992 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 993 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 994 | SKIP_INSTALL = YES; 995 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 996 | SWIFT_VERSION = 5.0; 997 | TARGET_NAME = DirectedGraph; 998 | TVOS_DEPLOYMENT_TARGET = 9.0; 999 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 1000 | }; 1001 | name = Debug; 1002 | }; 1003 | OBJ_63 /* Release */ = { 1004 | isa = XCBuildConfiguration; 1005 | buildSettings = { 1006 | ENABLE_TESTABILITY = YES; 1007 | FRAMEWORK_SEARCH_PATHS = ( 1008 | "$(inherited)", 1009 | "$(PLATFORM_DIR)/Developer/Library/Frameworks", 1010 | ); 1011 | HEADER_SEARCH_PATHS = "$(inherited)"; 1012 | INFOPLIST_FILE = DirectedGraph.xcodeproj/DirectedGraph_Info.plist; 1013 | IPHONEOS_DEPLOYMENT_TARGET = 13.0; 1014 | LD_RUNPATH_SEARCH_PATHS = ( 1015 | "$(inherited)", 1016 | "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx", 1017 | ); 1018 | MACOSX_DEPLOYMENT_TARGET = 10.15; 1019 | OTHER_CFLAGS = "$(inherited)"; 1020 | OTHER_LDFLAGS = "$(inherited)"; 1021 | OTHER_SWIFT_FLAGS = "$(inherited)"; 1022 | PRODUCT_BUNDLE_IDENTIFIER = DirectedGraph; 1023 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 1024 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 1025 | SKIP_INSTALL = YES; 1026 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; 1027 | SWIFT_VERSION = 5.0; 1028 | TARGET_NAME = DirectedGraph; 1029 | TVOS_DEPLOYMENT_TARGET = 9.0; 1030 | WATCHOS_DEPLOYMENT_TARGET = 2.0; 1031 | }; 1032 | name = Release; 1033 | }; 1034 | OBJ_97 /* Debug */ = { 1035 | isa = XCBuildConfiguration; 1036 | buildSettings = { 1037 | LD = /usr/bin/true; 1038 | OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -package-description-version 5.2.0"; 1039 | SWIFT_VERSION = 5.0; 1040 | }; 1041 | name = Debug; 1042 | }; 1043 | OBJ_98 /* Release */ = { 1044 | isa = XCBuildConfiguration; 1045 | buildSettings = { 1046 | LD = /usr/bin/true; 1047 | OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -package-description-version 5.2.0"; 1048 | SWIFT_VERSION = 5.0; 1049 | }; 1050 | name = Release; 1051 | }; 1052 | /* End XCBuildConfiguration section */ 1053 | 1054 | /* Begin XCConfigurationList section */ 1055 | 03AF06C32486DD47003ED12C /* Build configuration list for PBXNativeTarget "DirectedGraphDemo" */ = { 1056 | isa = XCConfigurationList; 1057 | buildConfigurations = ( 1058 | 03AF06C12486DD47003ED12C /* Debug */, 1059 | 03AF06C22486DD47003ED12C /* Release */, 1060 | ); 1061 | defaultConfigurationIsVisible = 0; 1062 | defaultConfigurationName = Release; 1063 | }; 1064 | OBJ_102 /* Build configuration list for PBXAggregateTarget "DirectedGraphPackageTests" */ = { 1065 | isa = XCConfigurationList; 1066 | buildConfigurations = ( 1067 | OBJ_103 /* Debug */, 1068 | OBJ_104 /* Release */, 1069 | ); 1070 | defaultConfigurationIsVisible = 0; 1071 | defaultConfigurationName = Release; 1072 | }; 1073 | OBJ_107 /* Build configuration list for PBXNativeTarget "DirectedGraphTests" */ = { 1074 | isa = XCConfigurationList; 1075 | buildConfigurations = ( 1076 | OBJ_108 /* Debug */, 1077 | OBJ_109 /* Release */, 1078 | ); 1079 | defaultConfigurationIsVisible = 0; 1080 | defaultConfigurationName = Release; 1081 | }; 1082 | OBJ_2 /* Build configuration list for PBXProject "DirectedGraph" */ = { 1083 | isa = XCConfigurationList; 1084 | buildConfigurations = ( 1085 | OBJ_3 /* Debug */, 1086 | OBJ_4 /* Release */, 1087 | ); 1088 | defaultConfigurationIsVisible = 0; 1089 | defaultConfigurationName = Release; 1090 | }; 1091 | OBJ_61 /* Build configuration list for PBXNativeTarget "DirectedGraph" */ = { 1092 | isa = XCConfigurationList; 1093 | buildConfigurations = ( 1094 | OBJ_62 /* Debug */, 1095 | OBJ_63 /* Release */, 1096 | ); 1097 | defaultConfigurationIsVisible = 0; 1098 | defaultConfigurationName = Release; 1099 | }; 1100 | OBJ_96 /* Build configuration list for PBXNativeTarget "DirectedGraphPackageDescription" */ = { 1101 | isa = XCConfigurationList; 1102 | buildConfigurations = ( 1103 | OBJ_97 /* Debug */, 1104 | OBJ_98 /* Release */, 1105 | ); 1106 | defaultConfigurationIsVisible = 0; 1107 | defaultConfigurationName = Release; 1108 | }; 1109 | /* End XCConfigurationList section */ 1110 | 1111 | /* Begin XCRemoteSwiftPackageReference section */ 1112 | 03341E3C248709EB00AAC9A8 /* XCRemoteSwiftPackageReference "ViewInspector" */ = { 1113 | isa = XCRemoteSwiftPackageReference; 1114 | repositoryURL = "https://github.com/nalexn/ViewInspector"; 1115 | requirement = { 1116 | kind = upToNextMajorVersion; 1117 | minimumVersion = 0.3.11; 1118 | }; 1119 | }; 1120 | /* End XCRemoteSwiftPackageReference section */ 1121 | 1122 | /* Begin XCSwiftPackageProductDependency section */ 1123 | 03341E3D248709EB00AAC9A8 /* ViewInspector */ = { 1124 | isa = XCSwiftPackageProductDependency; 1125 | package = 03341E3C248709EB00AAC9A8 /* XCRemoteSwiftPackageReference "ViewInspector" */; 1126 | productName = ViewInspector; 1127 | }; 1128 | /* End XCSwiftPackageProductDependency section */ 1129 | }; 1130 | rootObject = OBJ_1 /* Project object */; 1131 | } 1132 | -------------------------------------------------------------------------------- /DirectedGraph.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /DirectedGraph.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /DirectedGraph.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | -------------------------------------------------------------------------------- /DirectedGraph.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "ViewInspector", 6 | "repositoryURL": "https://github.com/nalexn/ViewInspector", 7 | "state": { 8 | "branch": null, 9 | "revision": "63b8b90a39febacce9afde79fc048cd2f2e17805", 10 | "version": "0.3.12" 11 | } 12 | } 13 | ] 14 | }, 15 | "version": 1 16 | } 17 | -------------------------------------------------------------------------------- /DirectedGraph.xcodeproj/xcshareddata/xcschemes/DirectedGraph-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 53 | 54 | 64 | 65 | 71 | 72 | 74 | 75 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /DirectedGraph.xcodeproj/xcshareddata/xcschemes/DirectedGraphDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 43 | 45 | 51 | 52 | 53 | 54 | 60 | 62 | 68 | 69 | 70 | 71 | 73 | 74 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /DirectedGraphDemo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // DirectedGraphDemo 4 | // 5 | // Created by Nicolas Mandica on 02/06/2020. 6 | // 7 | 8 | import UIKit 9 | 10 | @UIApplicationMain 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /DirectedGraphDemo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /DirectedGraphDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /DirectedGraphDemo/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /DirectedGraphDemo/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import DirectedGraph 3 | 4 | struct ContentView: View { 5 | static private let graph = try? SimpleGraph.load(filename: "graph") 6 | private let viewModel = GraphViewModel(Self.graph!) 7 | 8 | var body: some View { 9 | VStack { 10 | GraphView(viewModel) 11 | 12 | HStack { 13 | Button("Release Nodes") { 14 | self.viewModel.releaseNodes() 15 | } 16 | 17 | Spacer() 18 | 19 | Button("Toggle Edge Values") { 20 | self.viewModel.toggleEdgeValues() 21 | } 22 | }.padding() 23 | } 24 | } 25 | } 26 | 27 | struct ContentView_Previews: PreviewProvider { 28 | static var previews: some View { 29 | ContentView() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /DirectedGraphDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UIApplicationSceneManifest 24 | 25 | UIApplicationSupportsMultipleScenes 26 | 27 | UISceneConfigurations 28 | 29 | UIWindowSceneSessionRoleApplication 30 | 31 | 32 | UISceneConfigurationName 33 | Default Configuration 34 | UISceneDelegateClassName 35 | $(PRODUCT_MODULE_NAME).SceneDelegate 36 | 37 | 38 | 39 | 40 | UILaunchStoryboardName 41 | LaunchScreen 42 | UIRequiredDeviceCapabilities 43 | 44 | armv7 45 | 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /DirectedGraphDemo/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /DirectedGraphDemo/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | 4 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 5 | var window: UIWindow? 6 | 7 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 8 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 9 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 10 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 11 | 12 | // Create the SwiftUI view that provides the window contents. 13 | let contentView = ContentView() 14 | 15 | // Use a UIHostingController as window root view controller. 16 | if let windowScene = scene as? UIWindowScene { 17 | let window = UIWindow(windowScene: windowScene) 18 | window.rootViewController = UIHostingController(rootView: contentView) 19 | self.window = window 20 | window.makeKeyAndVisible() 21 | } 22 | } 23 | 24 | func sceneDidDisconnect(_ scene: UIScene) { 25 | // Called as the scene is being released by the system. 26 | // This occurs shortly after the scene enters the background, or when its session is discarded. 27 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 28 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead). 29 | } 30 | 31 | func sceneDidBecomeActive(_ scene: UIScene) { 32 | // Called when the scene has moved from an inactive state to an active state. 33 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 34 | } 35 | 36 | func sceneWillResignActive(_ scene: UIScene) { 37 | // Called when the scene will move from an active state to an inactive state. 38 | // This may occur due to temporary interruptions (ex. an incoming phone call). 39 | } 40 | 41 | func sceneWillEnterForeground(_ scene: UIScene) { 42 | // Called as the scene transitions from the background to the foreground. 43 | // Use this method to undo the changes made on entering the background. 44 | } 45 | 46 | func sceneDidEnterBackground(_ scene: UIScene) { 47 | // Called as the scene transitions from the foreground to the background. 48 | // Use this method to save data, release shared resources, and store enough scene-specific state information 49 | // to restore the scene back to its current state. 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /DirectedGraphDemo/graph.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | {"id": "A", "group": 1}, 4 | {"id": "B", "group": 2}, 5 | {"id": "C", "group": 2}, 6 | {"id": "D", "group": 2}, 7 | {"id": "E", "group": 2}, 8 | {"id": "F", "group": 2}, 9 | {"id": "G", "group": 2}, 10 | {"id": "G1", "group": 3}, 11 | {"id": "G2", "group": 3}, 12 | {"id": "G3", "group": 3}, 13 | {"id": "C1", "group": 3}, 14 | {"id": "C2", "group": 3}, 15 | {"id": "G31", "group": 4}, 16 | ], 17 | "edges": [ 18 | {"source": "A", "target": "B", "value": 4}, 19 | {"source": "A", "target": "C", "value": 4}, 20 | {"source": "A", "target": "D", "value": 4}, 21 | {"source": "A", "target": "E", "value": 4}, 22 | {"source": "A", "target": "F", "value": 4}, 23 | {"source": "A", "target": "G", "value": 6}, 24 | {"source": "G", "target": "G1", "value": 4}, 25 | {"source": "G", "target": "G2", "value": 4}, 26 | {"source": "G", "target": "G3", "value": 4}, 27 | {"source": "C", "target": "C1", "value": 2}, 28 | {"source": "C", "target": "C2", "value": 2}, 29 | {"source": "G3", "target": "G31", "value": 4}, 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 nmandica 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Media/Example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmandica/DirectedGraph/4aba72560eafae0f778666d0bc08376737dacd9a/Media/Example1.png -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "DirectedGraph", 8 | platforms: [ 9 | .macOS(.v10_15), 10 | .iOS(.v13) 11 | ], 12 | products: [ 13 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 14 | .library( 15 | name: "DirectedGraph", 16 | targets: ["DirectedGraph"]), 17 | ], 18 | dependencies: [ 19 | // Dependencies declare other packages that this package depends on. 20 | // .package(url: /* package url */, from: "1.0.0"), 21 | ], 22 | targets: [ 23 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 24 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 25 | .target( 26 | name: "DirectedGraph", 27 | dependencies: [], 28 | path: "Sources"), 29 | .testTarget( 30 | name: "DirectedGraphTests", 31 | dependencies: ["DirectedGraph"], 32 | path: "Tests"), 33 | ] 34 | ) 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DirectedGraph 2 | 3 |

4 | Build status 5 | Swift 5 compatible 6 | SwiftPM compatible 7 | Maintainability 8 | Platform iOS 9 | Platform macOS 10 | GitHub license 11 |

12 | 13 | SwiftUI package for displaying directed graphs. 14 | 15 |

16 | DirectedGraph example 17 |

18 | 19 | ## Installation 20 | 21 | In Xcode go to `File -> Swift Packages -> Add Package Dependency…` and paste the repo's url: `https://github.com/nmandica/DirectedGraph` 22 | 23 | ## Usage 24 | 25 | Import the package in the file you would like to use it: `import DirectedGraph` 26 | 27 | You can display a graph by adding a `GraphView` to your view. 28 | 29 | ## Minimum Requirements 30 | 31 | | DirectedGraph | Swift | Xcode | Platforms | 32 | |------------------------|-------------|----------------|------------------------------| 33 | | DirectedGraph 0.1 | Swift 5.2 | Xcode 11.0 | iOS 13.0 / macOS 10.15 | 34 | 35 | ## Thanks 36 | 37 | - [ViewInspector](https://github.com/nalexn/ViewInspector) 38 | 39 | ## License 40 | 41 | DirectedGraph is available under the MIT license. See the LICENSE file for more info. 42 | -------------------------------------------------------------------------------- /Sources/Extensions/ArrayExtensions.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension Array { 4 | var countDistinct: Int { 5 | NSSet(array: self).count 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Sources/Extensions/CGPointExtensions.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension CGPoint { 4 | @inlinable 5 | init(_ x: CGFloat, _ y: CGFloat) { 6 | self.init(x: x, y: y) 7 | } 8 | 9 | @inlinable 10 | static func += (lhs: inout CGPoint, rhs: CGPoint) { 11 | lhs.x += rhs.x 12 | lhs.y += rhs.y 13 | } 14 | 15 | @inlinable 16 | static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint { 17 | var copy = lhs 18 | copy += rhs 19 | return copy 20 | } 21 | 22 | @inlinable 23 | static prefix func - (point: CGPoint) -> CGPoint { 24 | return CGPoint(-point.x, -point.y) 25 | } 26 | 27 | @inlinable 28 | static func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint { 29 | return lhs + (-rhs) 30 | } 31 | 32 | @inlinable 33 | static func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint { 34 | return CGPoint(lhs.x * rhs, lhs.y * rhs) 35 | } 36 | 37 | @inlinable 38 | static func / (lhs: CGPoint, rhs: CGFloat) -> CGPoint { 39 | return CGPoint(lhs.x / rhs, lhs.y / rhs) 40 | } 41 | 42 | @inlinable 43 | var lengthSquared: CGFloat { 44 | return x * x + y * y 45 | } 46 | 47 | @inlinable 48 | var length: CGFloat { 49 | return lengthSquared.squareRoot() 50 | } 51 | 52 | @inlinable 53 | var angle: CGFloat { 54 | return atan2(y, x) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Extensions/CGRectExtensions.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension CGRect { 4 | @inlinable 5 | var center: CGPoint { 6 | return CGPoint(x: midX, y: midY) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/Extensions/CGSizeExtensions.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension CGSize { 4 | @inlinable 5 | static func + (lhs: CGSize, rhs: CGSize) -> CGSize { 6 | return CGSize(width: lhs.width + rhs.width, height: lhs.height + rhs.height) 7 | } 8 | 9 | @inlinable 10 | static func / (lhs: CGSize, rhs: CGFloat) -> CGSize { 11 | return CGSize(width: lhs.width / rhs, height: lhs.height / rhs) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/Extensions/CollectionExtensions.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Collection where Element == CGPoint { 4 | func averagePoint() -> CGPoint? { 5 | guard !isEmpty else { 6 | return nil 7 | } 8 | 9 | return reduce(.zero, +) / CGFloat(count) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Extensions/ComparableExtensions.swift: -------------------------------------------------------------------------------- 1 | extension Comparable { 2 | func clamped(to limits: ClosedRange) -> Self { 3 | return min(max(self, limits.lowerBound), limits.upperBound) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Sources/Graph/Edge.swift: -------------------------------------------------------------------------------- 1 | /// An edge is the directed link between two nodes 2 | public protocol Edge: Codable { 3 | var source: String { get } 4 | var target: String { get } 5 | var value: Int { get } 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Graph/Graph.swift: -------------------------------------------------------------------------------- 1 | /// A graph is a collection of nodes and edges between the nodes 2 | public protocol Graph: Codable { 3 | associatedtype NodeType: Node 4 | associatedtype EdgeType: Edge 5 | 6 | var nodes: [NodeType] { get } 7 | var edges: [EdgeType] { get } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/Graph/Node.swift: -------------------------------------------------------------------------------- 1 | /// A node represents a vertex of the graph (a dot) 2 | public protocol Node: Codable { 3 | var id: String { get } 4 | } 5 | -------------------------------------------------------------------------------- /Sources/Graph/SimpleEdge.swift: -------------------------------------------------------------------------------- 1 | public struct SimpleEdge: Edge { 2 | public var source: String 3 | public var target: String 4 | public var value: Int 5 | 6 | public init(source: String, target: String, value: Int) { 7 | self.source = source 8 | self.target = target 9 | self.value = value 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Graph/SimpleGraph.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public struct SimpleGraph: Graph { 4 | public var nodes: [SimpleNode] 5 | public var edges: [SimpleEdge] 6 | 7 | public init(nodes: [SimpleNode], edges: [SimpleEdge]) { 8 | self.nodes = nodes 9 | self.edges = edges 10 | } 11 | } 12 | 13 | extension SimpleGraph { 14 | enum Error: Swift.Error { 15 | case fileNotFound(String) 16 | } 17 | 18 | private init(jsonData: Data) throws { 19 | let decoder = JSONDecoder() 20 | let decoded = try decoder.decode(Self.self, from: jsonData) 21 | self.init(nodes: decoded.nodes, edges: decoded.edges) 22 | } 23 | 24 | /** 25 | Create a ```Graph``` from a JSON file. 26 | 27 | # Example 28 | If you want to load the _graph.json_ file 29 | ``` 30 | let graph = try! SimpleGraph.load(filename: "graph") 31 | ``` 32 | */ 33 | public static func load(filename: String, bundle: Bundle = Bundle.main) throws -> Self { 34 | guard let url = bundle.url(forResource: filename, withExtension: "json") else { 35 | throw Error.fileNotFound(filename) 36 | } 37 | 38 | let data = try Data(contentsOf: url) 39 | return try Self(jsonData: data) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/Graph/SimpleNode.swift: -------------------------------------------------------------------------------- 1 | public struct SimpleNode: Node { 2 | public var id: String 3 | public var group: Int 4 | 5 | public init(id: String, group: Int) { 6 | self.id = id 7 | self.group = group 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Sources/Layouts/CircularLayoutEngine.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// A layout engine giving a circular graph 4 | public struct CircularLayoutEngine: LayoutEngine { 5 | public let isIncremental = false 6 | 7 | public init() { } 8 | 9 | public func layout(from layout: Layout, canvas: CGRect, edgeIndices: [[Int]]) -> Layout { 10 | let count = layout.itemCount 11 | let radius = min(canvas.width, canvas.height) * 0.4 12 | let center = canvas.center 13 | let delta = 2 * CGFloat.pi / CGFloat(count) 14 | 15 | var angle = CGFloat(0) 16 | let items = (0.. LayoutItem in 17 | let position = center + CGPoint(cos(angle), sin(angle)) * radius 18 | angle += delta 19 | return LayoutItem(position: position, velocity: CGPoint.zero) 20 | } 21 | 22 | return Layout(items: items) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/Layouts/ForceDirectedLayoutEngine.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// A layout engine giving a force-directed graph 4 | public struct ForceDirectedLayoutEngine: LayoutEngine { 5 | private let steps = 3 6 | public let isIncremental = true 7 | public var viscosity: CGFloat = 20 8 | public var friction: CGFloat = 0.7 9 | public var springLength: CGFloat = 70 10 | public var stiffness: CGFloat = 0.09 11 | public var charge: CGFloat = 50 12 | public var gravitationalConstant: CGFloat = 20 13 | public var shieldDistanceSquared: CGFloat = 250000 14 | 15 | public init() { } 16 | 17 | public func layout(from layout: Layout, canvas: CGRect, edgeIndices: [[Int]]) -> Layout { 18 | var positions = layout.items.map { $0.position } 19 | var velocities = layout.items.map { $0.velocity } 20 | for _ in 1...steps { 21 | var forces = Array(repeating: CGPoint.zero, count: layout.itemCount) 22 | let edges = edgeIndices.map { $0.map { positions[$0] } } 23 | let center = canvas.center - (positions.averagePoint() ?? .zero) 24 | for (index, position) in positions.enumerated() { 25 | forces[index] += repulsionForce(at: position, from: positions, skipIndex: index) 26 | forces[index] += springForce(at: position, from: edges[index]) 27 | forces[index] += centralForce(at: position, from: canvas.center) 28 | 29 | let nv = velocities[index] + forces[index] 30 | let d = nv.length 31 | 32 | velocities[index] = d > viscosity ? nv / d * viscosity : nv * friction 33 | positions[index] += center + velocities[index] 34 | } 35 | } 36 | 37 | let items = Array(zip(positions, velocities)) 38 | return Layout(items: items) 39 | } 40 | 41 | private func springForce(at source: CGPoint, from targets: [CGPoint]) -> CGPoint { 42 | var force = CGPoint.zero 43 | for target in targets { 44 | force += springForce(at: source, from: target) 45 | } 46 | 47 | return force 48 | } 49 | 50 | private func springForce(at source: CGPoint, from target: CGPoint) -> CGPoint { 51 | let delta = target - source 52 | let length = delta.length 53 | let normalized = length > 0 ? delta / length : .zero 54 | return normalized * (length - springLength) * stiffness 55 | } 56 | 57 | private func repulsionForce(at point: CGPoint, from others: [CGPoint], skipIndex skippedIndex: Int) -> CGPoint { 58 | var force = CGPoint.zero 59 | for (index, other) in others.enumerated() { 60 | guard index != skippedIndex else { continue } 61 | 62 | let diff = point - other 63 | let diffSquared = diff.lengthSquared 64 | guard diffSquared < shieldDistanceSquared else { 65 | continue 66 | } 67 | force += diff / (diffSquared + 0.00000001) * charge 68 | } 69 | 70 | return force 71 | } 72 | 73 | private func centralForce(at point: CGPoint, from center: CGPoint) -> CGPoint { 74 | let diff = center - point 75 | let dist = diff.lengthSquared 76 | return dist > shieldDistanceSquared ? diff / dist * gravitationalConstant : .zero 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/Layouts/Layout.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct Layout { 4 | let items: [LayoutItem] 5 | let itemCount: Int 6 | 7 | init(items: [LayoutItem]) { 8 | self.items = items 9 | self.itemCount = items.count 10 | } 11 | 12 | init(items: [(position: CGPoint, velocity: CGPoint)]) { 13 | self.init(items: items.map { 14 | LayoutItem(position: $0.position, velocity: $0.velocity) 15 | }) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Layouts/LayoutEngine.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// A way to compute a graph layout 4 | public protocol LayoutEngine { 5 | var isIncremental: Bool { get } 6 | 7 | func layout(from layout: Layout, 8 | canvas: CGRect, 9 | edgeIndices: [[Int]]) -> Layout 10 | } 11 | -------------------------------------------------------------------------------- /Sources/Layouts/LayoutItem.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct LayoutItem { 4 | let position: CGPoint 5 | let velocity: CGPoint 6 | } 7 | -------------------------------------------------------------------------------- /Sources/Layouts/RandomLayoutEngine.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// A layout engine giving a random graph layout 4 | public struct RandomLayoutEngine: LayoutEngine { 5 | public let isIncremental = false 6 | 7 | public init() { } 8 | 9 | public func layout(from layout: Layout, canvas: CGRect, edgeIndices: [[Int]]) -> Layout { 10 | let count = layout.itemCount 11 | let items = (0.. LayoutItem in 12 | let position = CGPoint( 13 | CGFloat.random(in: 0.. Color { 14 | return colors[index % colorCount] 15 | } 16 | 17 | private static func buildColors(_ count: Int) -> [Color] { 18 | return (0.. Color { 22 | return Angle(radians: Double(index) / Double(colorCount) * 2.0 * .pi).color 23 | } 24 | } 25 | 26 | extension Angle { 27 | var color: Color { 28 | Color(hue: self.radians / (2 * .pi), saturation: 1, brightness: 0.8) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/Utilities/Screen.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | class Screen { 4 | public static var bounds: CGRect { 5 | #if canImport(UIKit) 6 | return UIScreen.main.bounds 7 | #else 8 | return NSScreen.main?.frame ?? CGRect() 9 | #endif 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/Utilities/SizeReader.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension View { 4 | func sizeReader(_ size: Binding) -> some View { 5 | SizeReader(size: size) { 6 | self 7 | } 8 | } 9 | } 10 | 11 | struct SizeReader: View { 12 | @Binding var size: CGSize 13 | let content: () -> Content 14 | var body: some View { 15 | ZStack { 16 | content() 17 | .background( 18 | GeometryReader { proxy in 19 | Color.clear 20 | .preference(key: SizePreferenceKey.self, value: proxy.size) 21 | } 22 | ) 23 | } 24 | .onPreferenceChange(SizePreferenceKey.self) { preferences in 25 | self.size = preferences 26 | } 27 | } 28 | } 29 | 30 | private struct SizePreferenceKey: PreferenceKey { 31 | typealias Value = CGSize 32 | static var defaultValue: Value = .zero 33 | 34 | static func reduce(value _: inout Value, nextValue: () -> Value) { 35 | _ = nextValue() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Views/Arrow.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct Arrow: Shape { 4 | private let pointerLineLength: CGFloat = 30 5 | private let arrowAngle = CGFloat(Double.pi / 6) 6 | let start: CGPoint 7 | let end: CGPoint 8 | let thickness: CGFloat 9 | 10 | func path(in rect: CGRect) -> Path { 11 | var path = Path() 12 | 13 | path.move(to: start) 14 | path.addLine(to: end) 15 | 16 | let delta = end - start 17 | let angle = delta.angle 18 | let arrowLine1 = CGPoint(x: end.x + pointerLineLength * cos(CGFloat(Double.pi) - angle + arrowAngle), 19 | y: end.y - pointerLineLength * sin(CGFloat(Double.pi) - angle + arrowAngle)) 20 | let arrowLine2 = CGPoint(x: end.x + pointerLineLength * cos(CGFloat(Double.pi) - angle - arrowAngle), 21 | y: end.y - pointerLineLength * sin(CGFloat(Double.pi) - angle - arrowAngle)) 22 | 23 | path.move(to: arrowLine1) 24 | path.addLine(to: end) 25 | path.addLine(to: arrowLine2) 26 | 27 | return path.strokedPath(.init(lineWidth: thickness)) 28 | } 29 | } 30 | 31 | struct Arrow_Previews: PreviewProvider { 32 | static let start = CGPoint(x: 80, y: 80) 33 | static let end = CGPoint(x: 300, y: 200) 34 | 35 | static var previews: some View { 36 | Arrow(start: start, end: end, thickness: 4) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/Views/DefaultNodeView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct DefaultNodeView: View { 4 | @State private var size: CGSize = .zero 5 | private let id: String 6 | private let color: Color 7 | 8 | init(node: SimpleNode, palette: Palette) { 9 | id = node.id 10 | color = palette.color(for: node.group) 11 | } 12 | 13 | public var body: some View { 14 | ZStack { 15 | Ellipse() 16 | .foregroundColor(color) 17 | .frame(width: max(size.width, size.height), height: size.height) 18 | 19 | Text(id) 20 | .padding(10) 21 | .sizeReader($size) 22 | .colorInvert() 23 | .shadow(radius: 1) 24 | } 25 | } 26 | } 27 | 28 | struct DefaultNodeView_Previews: PreviewProvider { 29 | static var previews: some View { 30 | DefaultNodeView(node: SimpleNode(id: "A2", group: 0), palette: Palette(colorCount: 1)) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/Views/EdgeView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Combine 3 | 4 | struct EdgeView: View { 5 | @ObservedObject var viewModel: EdgeViewModel 6 | 7 | var body: some View { 8 | ZStack { 9 | Arrow(start: viewModel.start, end: viewModel.end, thickness: viewModel.value) 10 | .foregroundColor(.gray) 11 | .opacity(0.5) 12 | 13 | if viewModel.showValue { 14 | Text(viewModel.value.description) 15 | .font(.caption) 16 | .position(viewModel.middle) 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Sources/Views/EdgeViewModel.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Combine 3 | 4 | final class EdgeViewModel: ObservableObject, Identifiable { 5 | let id: String 6 | let value: CGFloat 7 | @Published private var source: NodeViewModel 8 | @Published private var target: NodeViewModel 9 | @Published var showValue = false 10 | var sourceCancellable: AnyCancellable? 11 | var targetCancellable: AnyCancellable? 12 | 13 | init(source: NodeViewModel, target: NodeViewModel, value: CGFloat) { 14 | self.id = "\(source.id)-\(target.id)" 15 | self.source = source 16 | self.target = target 17 | self.value = value 18 | 19 | sourceCancellable = source.objectWillChange.sink { (_) in 20 | self.objectWillChange.send() 21 | } 22 | targetCancellable = target.objectWillChange.sink { (_) in 23 | self.objectWillChange.send() 24 | } 25 | } 26 | 27 | var middle: CGPoint { (source.position + target.position) / 2 } 28 | 29 | var start: CGPoint { 30 | source.position 31 | } 32 | 33 | var end: CGPoint { 34 | let delta = target.position - start 35 | let angle = delta.angle 36 | let suppr = CGPoint(x: cos(angle) * (target.size.width + value) * 0.5, y: sin(angle) * (target.size.height + value) * 0.5) 37 | return target.position - suppr 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Views/GraphView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | public struct GraphView: View { 4 | @ObservedObject private var viewModel: GraphViewModel 5 | @State private var currentOffset = CGSize.zero 6 | @State private var finalOffset = CGSize.zero 7 | @State private var scale: CGFloat = 1 8 | private let nodeContent: (Graph.NodeType) -> NodeContent 9 | 10 | public init(_ viewModel: GraphViewModel, @ViewBuilder nodeContent: @escaping (Graph.NodeType) -> NodeContent) { 11 | self.viewModel = viewModel 12 | self.nodeContent = nodeContent 13 | } 14 | 15 | public var body: some View { 16 | let offset = finalOffset + currentOffset 17 | let scroll = DragGesture() 18 | .onChanged { gesture in 19 | self.currentOffset = gesture.translation / self.scale 20 | } 21 | .onEnded { _ in 22 | self.finalOffset = offset 23 | self.currentOffset = CGSize.zero 24 | } 25 | 26 | return ZStack { 27 | ForEach(viewModel.edges) { edge in 28 | EdgeView(viewModel: edge) 29 | } 30 | 31 | ForEach(viewModel.nodes) { node in 32 | NodeView(viewModel: node) { 33 | self.nodeContent((node.node as? Graph.NodeType)!) 34 | } 35 | } 36 | } 37 | .offset(offset) 38 | .scaleEffect(scale) 39 | .contentShape(Rectangle()) 40 | .gesture(scroll) 41 | .scalable(initialScale: self.$scale, scaleRange: CGFloat(0.2)...5) 42 | .onAppear { 43 | self.viewModel.startLayout() 44 | } 45 | } 46 | } 47 | 48 | struct GraphView_Previews: PreviewProvider { 49 | private static let nodes = [ 50 | SimpleNode(id: "1", group: 0), 51 | SimpleNode(id: "2", group: 0), 52 | SimpleNode(id: "3", group: 1), 53 | SimpleNode(id: "4", group: 2)] 54 | 55 | private static let edges = [ 56 | SimpleEdge(source: "1", target: "2", value: 5), 57 | SimpleEdge(source: "1", target: "3", value: 1), 58 | SimpleEdge(source: "3", target: "4", value: 2), 59 | SimpleEdge(source: "2", target: "3", value: 1) 60 | ] 61 | 62 | static var previews: some View { 63 | GraphView(GraphViewModel(SimpleGraph(nodes: nodes, 64 | edges: edges))) 65 | } 66 | } 67 | 68 | public extension GraphView where NodeContent == DefaultNodeView, Graph == SimpleGraph { 69 | init(_ viewModel: GraphViewModel) { 70 | let count = viewModel.graphNodes.compactMap { $0.group }.countDistinct 71 | let palette = Palette(colorCount: count) 72 | self.init(viewModel) { node in 73 | DefaultNodeView(node: node, palette: palette) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Sources/Views/GraphViewModel.swift: -------------------------------------------------------------------------------- 1 | import Combine 2 | import SwiftUI 3 | 4 | final public class GraphViewModel: ObservableObject { 5 | private var timer: Timer? 6 | private var isSimulating: Bool { timer != nil } 7 | private var edgeIndices: [[Int]] 8 | private(set) var nodes: [NodeViewModel] 9 | private(set) var edges: [EdgeViewModel] 10 | let graphNodes: [Graph.NodeType] 11 | var willChange = PassthroughSubject() 12 | 13 | public var layoutEngine: LayoutEngine = ForceDirectedLayoutEngine() 14 | 15 | public init(_ graph: Graph) { 16 | nodes = Self.buildNodes(graph) 17 | edges = Self.buildEdges(graph.edges, nodeViewModels: nodes) 18 | graphNodes = graph.nodes 19 | edgeIndices = Self.buildEdgeIndices(graph) 20 | } 21 | 22 | public func releaseNodes() { 23 | for node in nodes { 24 | node.interactive = false 25 | } 26 | } 27 | 28 | public func toggleEdgeValues() { 29 | for edge in edges { 30 | edge.showValue.toggle() 31 | } 32 | } 33 | 34 | public func toggleAutoLayout() { 35 | isSimulating ? stopLayout() : startLayout() 36 | } 37 | 38 | private static func buildNodes(_ graph: Graph) -> [NodeViewModel] { 39 | return graph.nodes.map { NodeViewModel($0) } 40 | } 41 | 42 | private static func buildEdges(_ edges: [Edge], nodeViewModels: [NodeViewModel]) -> [EdgeViewModel] { 43 | let nodeViewModelLookup = Dictionary(uniqueKeysWithValues: nodeViewModels.map { ($0.id, $0) }) 44 | let viewModels: [EdgeViewModel] = edges.compactMap { 45 | guard let source = nodeViewModelLookup[$0.source], 46 | let target = nodeViewModelLookup[$0.target] else { 47 | return nil 48 | } 49 | return EdgeViewModel(source: source, target: target, value: CGFloat($0.value)) 50 | } 51 | 52 | return viewModels 53 | } 54 | 55 | private static func buildEdgeIndices(_ graph: Graph) -> [[Int]] { 56 | let nodeIndexLookup = Dictionary(uniqueKeysWithValues: graph.nodes.enumerated().map { ($0.1.id, $0.0) }) 57 | var edgeIndices = Array(repeating: [Int](), count: graph.nodes.count) 58 | 59 | for edge in graph.edges { 60 | guard let a = nodeIndexLookup[edge.source], 61 | let b = nodeIndexLookup[edge.target] else { 62 | continue 63 | } 64 | edgeIndices[a].append(b) 65 | edgeIndices[b].append(a) 66 | } 67 | 68 | return edgeIndices 69 | } 70 | 71 | public func startLayout() { 72 | guard layoutEngine.isIncremental else { 73 | self.computeLayout(engine: layoutEngine) 74 | return 75 | } 76 | 77 | guard !isSimulating else { return } 78 | 79 | timer = Timer.scheduledTimer(withTimeInterval: 0.08, repeats: true) { [weak self] _ in 80 | guard let self = self else { return } 81 | self.willChange.send() 82 | self.computeLayout(engine: self.layoutEngine) 83 | } 84 | RunLoop.main.add(timer!, forMode: .common) 85 | } 86 | 87 | public func stopLayout() { 88 | guard isSimulating else { return } 89 | 90 | timer?.invalidate() 91 | timer = nil 92 | } 93 | 94 | private func computeLayout(engine: LayoutEngine) { 95 | let currentLayout = Layout(items: nodes.map { 96 | LayoutItem(position: $0.position, velocity: $0.velocity) 97 | }) 98 | 99 | let layout = engine.layout(from: currentLayout, 100 | canvas: Screen.bounds, 101 | edgeIndices: edgeIndices) 102 | 103 | for (index, item) in layout.items.enumerated() { 104 | guard !nodes[index].interactive else { continue } 105 | nodes[index].position = item.position 106 | nodes[index].velocity = item.velocity 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Sources/Views/NodeView.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import SwiftUI 3 | 4 | struct NodeView: View { 5 | @ObservedObject private var viewModel: NodeViewModel 6 | private let content: () -> Content 7 | @State private var delta: CGPoint = .zero 8 | 9 | public init(viewModel: NodeViewModel, @ViewBuilder content: @escaping () -> Content) { 10 | self.viewModel = viewModel 11 | self.content = content 12 | } 13 | 14 | var body: some View { 15 | content() 16 | .sizeReader($viewModel.size) 17 | .position(viewModel.position) 18 | .gesture(drag) 19 | .onTapGesture(count: 2) { 20 | self.viewModel.interactive.toggle() 21 | } 22 | } 23 | 24 | private var drag: some Gesture { 25 | DragGesture() 26 | .onChanged { value in 27 | if self.delta == .zero { 28 | self.delta = value.location - self.viewModel.position 29 | } 30 | self.viewModel.interactive = true 31 | self.viewModel.position = value.location - self.delta 32 | self.viewModel.velocity = .zero 33 | } 34 | .onEnded { _ in 35 | self.viewModel.interactive = true 36 | self.delta = .zero 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/Views/NodeViewModel.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import Combine 3 | 4 | final class NodeViewModel: ObservableObject, Identifiable { 5 | let node: Node 6 | let id: String 7 | @Published var interactive = false 8 | @Published var position: CGPoint 9 | @Published var size: CGSize 10 | var velocity: CGPoint 11 | 12 | init(_ node: Node) { 13 | self.node = node 14 | id = node.id 15 | position = .zero 16 | velocity = .zero 17 | size = .zero 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/Views/ScalableView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct ScalableView: View { 4 | @State private var currentAmount: CGFloat = 0 5 | @Binding private var finalAmount: CGFloat 6 | let scaleRange: ClosedRange 7 | let content: () -> Content 8 | 9 | var body: some View { 10 | content() 11 | .scaleEffect(1 + currentAmount / finalAmount) 12 | .gesture( 13 | TapGesture(count: 2) 14 | .onEnded({ _ in 15 | withAnimation { 16 | self.finalAmount = self.finalAmount != 1.0 ? 1.0 : 2.0 17 | } 18 | })) 19 | .gesture( 20 | MagnificationGesture() 21 | .onChanged { amount in 22 | if self.finalAmount + amount - 1 >= 0 { 23 | self.currentAmount = amount - 1 24 | } else { 25 | self.currentAmount = -self.finalAmount 26 | } 27 | } 28 | .onEnded { _ in 29 | self.finalAmount = (self.finalAmount + self.currentAmount).clamped(to: self.scaleRange) 30 | self.currentAmount = 0 31 | } 32 | ) 33 | } 34 | 35 | public init(initialScale: Binding, scaleRange: ClosedRange, @ViewBuilder content: @escaping () -> Content) { 36 | self._finalAmount = initialScale 37 | self.scaleRange = scaleRange 38 | self.content = content 39 | } 40 | } 41 | 42 | extension View { 43 | func scalable(initialScale: Binding, scaleRange: ClosedRange = CGFloat(0.5)...10.0) -> some View { 44 | ScalableView(initialScale: initialScale, scaleRange: scaleRange) { 45 | self 46 | } 47 | } 48 | } 49 | 50 | struct ScalableView_Previews: PreviewProvider { 51 | @State static var scale: CGFloat = 1 52 | static var previews: some View { 53 | ScalableView(initialScale: $scale, scaleRange: CGFloat(0.2)...4.0) { 54 | Text("Test Text") 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Tests/DirectedGraphTests/XCTestManifests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | #if !canImport(ObjectiveC) 4 | public func allTests() -> [XCTestCaseEntry] { 5 | return [ 6 | testCase(DirectedGraphTests.allTests), 7 | ] 8 | } 9 | #endif 10 | -------------------------------------------------------------------------------- /Tests/Extensions/ArrayExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DirectedGraph 3 | 4 | class ArrayExtensionsTests: XCTestCase { 5 | override func setUpWithError() throws { 6 | // Put setup code here. This method is called before the invocation of each test method in the class. 7 | } 8 | 9 | override func tearDownWithError() throws { 10 | // Put teardown code here. This method is called after the invocation of each test method in the class. 11 | } 12 | 13 | func testCountDistinct() throws { 14 | let array = [1, 8, 92, 1, 1, 3, 2, 90, 92, -1] 15 | 16 | let count = array.countDistinct 17 | 18 | XCTAssertEqual(count, 7) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/Extensions/CGPointExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DirectedGraph 3 | 4 | class CGPointExtensionsTests: XCTestCase { 5 | func testAddition() { 6 | let point1 = CGPoint(x: 40, y: 121) 7 | let point2 = CGPoint(x: 50, y: 32) 8 | 9 | let added = point1 + point2 10 | 11 | XCTAssertEqual(added, CGPoint(x: 90, y: 153)) 12 | } 13 | 14 | func testSubtraction() { 15 | let point1 = CGPoint(x: 50, y: 1) 16 | let point2 = CGPoint(x: 10, y: 32) 17 | 18 | let substracted = point1 - point2 19 | 20 | XCTAssertEqual(substracted, CGPoint(x: 40, y: -31)) 21 | } 22 | 23 | func testOpposite() { 24 | let point = CGPoint(x: 50, y: -21) 25 | 26 | let opposite = -point 27 | 28 | XCTAssertEqual(opposite, CGPoint(x: -50, y: 21)) 29 | } 30 | 31 | func testAdditionCompoundAssignment() { 32 | var point1 = CGPoint(x: 40, y: 121) 33 | let point2 = CGPoint(x: 50, y: 32) 34 | 35 | point1 += point2 36 | 37 | XCTAssertEqual(point1, CGPoint(x: 90, y: 153)) 38 | } 39 | 40 | func testFloatMultiplication() { 41 | let point = CGPoint(x: 50, y: 2) 42 | 43 | let multiplied = point * 3 44 | 45 | XCTAssertEqual(multiplied, CGPoint(x: 150, y: 6)) 46 | } 47 | 48 | func testFloatDivision() { 49 | let point = CGPoint(x: 50, y: 2) 50 | 51 | let divided = point / 2 52 | 53 | XCTAssertEqual(divided, CGPoint(x: 25, y: 1)) 54 | } 55 | 56 | func testLengthSquared() { 57 | let point = CGPoint(x: 5, y: 4) 58 | 59 | let lengthSquared = point.lengthSquared 60 | 61 | XCTAssertEqual(lengthSquared, 41) 62 | } 63 | 64 | func testLength() { 65 | let point = CGPoint(x: 3, y: 4) 66 | 67 | let length = point.length 68 | 69 | XCTAssertEqual(length, 5) 70 | } 71 | 72 | func testAngle() { 73 | let point = CGPoint(x: 1, y: 1) 74 | XCTAssertEqual(point.angle, .pi / 4.0) 75 | } 76 | 77 | func testAngleWhenZeroReturnZero() { 78 | XCTAssertEqual(CGPoint.zero.angle, .zero) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tests/Extensions/CGRectExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DirectedGraph 3 | 4 | class CGRectExtensionsTests: XCTestCase { 5 | func testCenter() { 6 | let rect = CGRect(origin: CGPoint(x: 10, y: 40), size: CGSize(width: 50, height: 60)) 7 | 8 | let center = rect.center 9 | 10 | XCTAssertEqual(center, CGPoint(x: 35, y: 70)) 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/Extensions/CGSizeExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DirectedGraph 3 | 4 | class CGSizeExtensionsTests: XCTestCase { 5 | func testAddition() { 6 | let size1 = CGSize(width: 40, height: 121) 7 | let size2 = CGSize(width: 50, height: 32) 8 | 9 | let added = size1 + size2 10 | 11 | XCTAssertEqual(added, CGSize(width: 90, height: 153)) 12 | } 13 | 14 | func testDivision() { 15 | let size = CGSize(width: 40, height: 121) 16 | 17 | let divided = size / 10 18 | 19 | XCTAssertEqual(divided, CGSize(width: 4, height: 12.1)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/Extensions/CollectionExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DirectedGraph 3 | 4 | class CollectionExtensionsTests: XCTestCase { 5 | func testAveragePoint() { 6 | let point1 = CGPoint(x: 40, y: 121) 7 | let point2 = CGPoint(x: 50, y: 32) 8 | let array = [point1, point2] 9 | 10 | let average = array.averagePoint() 11 | 12 | XCTAssertEqual(average, CGPoint(x: 45, y: 76.5)) 13 | } 14 | 15 | func testWhenCollectionIsEmptyAveragePointReturnNil() { 16 | XCTAssertNil([CGPoint]().averagePoint()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/Extensions/ComparableExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DirectedGraph 3 | 4 | class ComparableExtensionsTests: XCTestCase { 5 | override func setUpWithError() throws { 6 | // Put setup code here. This method is called before the invocation of each test method in the class. 7 | } 8 | 9 | override func tearDownWithError() throws { 10 | // Put teardown code here. This method is called after the invocation of each test method in the class. 11 | } 12 | 13 | func testClampedWhenSmallerReturnMinValue() throws { 14 | let minValue = 3 15 | let maxValue = 10 16 | let range = minValue...maxValue 17 | 18 | let clamped = 2.clamped(to: range) 19 | 20 | XCTAssertEqual(clamped, minValue) 21 | } 22 | 23 | func testClampedWhenBiggerReturnMaxValue() throws { 24 | let minValue = 1.2 25 | let maxValue = 9.5 26 | let range = minValue...maxValue 27 | 28 | let clamped = 9.6.clamped(to: range) 29 | 30 | XCTAssertEqual(clamped, maxValue) 31 | } 32 | 33 | func testClampedWhenInRangeReturnValue() throws { 34 | let minValue = -1 35 | let maxValue = 6 36 | let value = 5 37 | let range = minValue...maxValue 38 | 39 | let clamped = value.clamped(to: range) 40 | 41 | XCTAssertEqual(clamped, value) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Tests/Utilities/PaletteTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DirectedGraph 3 | 4 | class PaletteTests: XCTestCase { 5 | override func setUpWithError() throws { 6 | // Put setup code here. This method is called before the invocation of each test method in the class. 7 | } 8 | 9 | override func tearDownWithError() throws { 10 | // Put teardown code here. This method is called after the invocation of each test method in the class. 11 | } 12 | 13 | func testColorForIndexIsCyclic() throws { 14 | let palette = Palette(colorCount: 3) 15 | 16 | let colorAt0 = palette.color(for: 0) 17 | let colorAt3 = palette.color(for: 3) 18 | 19 | XCTAssertEqual(colorAt0, colorAt3) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/Views/EdgeViewModelTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DirectedGraph 3 | 4 | class EdgeViewModelTests: XCTestCase { 5 | private let sourceNode = NodeViewModel(SimpleNode(id: "1", group: 3)) 6 | private let targetNode = NodeViewModel(SimpleNode(id: "2", group: 3)) 7 | private var cancellable: Any? 8 | 9 | func testEndIsNotNaNWhenSourceAndTargetAtSamePosition() { 10 | sourceNode.position = CGPoint(25, 40) 11 | targetNode.position = CGPoint(25, 40) 12 | 13 | let viewModel = EdgeViewModel(source: sourceNode, target: targetNode, value: .zero) 14 | 15 | XCTAssertFalse(viewModel.end.x.isNaN) 16 | } 17 | 18 | func testSourceChangeTriggerEdgeChange() { 19 | let viewModel = EdgeViewModel(source: sourceNode, target: targetNode, value: 40) 20 | var hasChanged = false 21 | cancellable = viewModel.objectWillChange.sink { _ in 22 | hasChanged = true 23 | } 24 | 25 | sourceNode.position.x += 2 26 | 27 | XCTAssertTrue(hasChanged) 28 | } 29 | 30 | func testTargetChangeTriggerEdgeChange() { 31 | let viewModel = EdgeViewModel(source: sourceNode, target: targetNode, value: 40) 32 | var hasChanged = false 33 | cancellable = viewModel.objectWillChange.sink { _ in 34 | hasChanged = true 35 | } 36 | 37 | targetNode.position.x += 2 38 | 39 | XCTAssertTrue(hasChanged) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/Views/EdgeViewTest.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftUI 3 | import ViewInspector 4 | @testable import DirectedGraph 5 | 6 | class EdgeViewTests: XCTestCase { 7 | private let nodeViewModel1 = NodeViewModel(SimpleNode(id: "1", group: 3)) 8 | private let nodeViewModel2 = NodeViewModel(SimpleNode(id: "2", group: 3)) 9 | 10 | func testStartAndEndPositions() throws { 11 | let position1 = CGPoint(x: 10, y: 20) 12 | let position2 = CGPoint(x: 60, y: -12) 13 | nodeViewModel1.position = position1 14 | nodeViewModel2.position = position2 15 | let viewModel = EdgeViewModel(source: nodeViewModel1, target: nodeViewModel2, value: 40) 16 | let view = makeView(viewModel) 17 | 18 | let arrow = try view.inspect().zStack().view(Arrow.self, 0).actualView() 19 | 20 | XCTAssertEqual(arrow.start, position1) 21 | XCTAssertEqual(arrow.end, viewModel.end) 22 | } 23 | 24 | func testWhenShowValueDisplayText() throws { 25 | let viewModel = EdgeViewModel(source: nodeViewModel1, target: nodeViewModel2, value: 40) 26 | viewModel.showValue = true 27 | let view = makeView(viewModel) 28 | 29 | let text = try view.inspect().zStack().text(1).string() 30 | 31 | XCTAssertEqual(text, "40.0") 32 | } 33 | 34 | func makeView(_ viewModel: EdgeViewModel) -> EdgeView { 35 | return EdgeView(viewModel: viewModel) 36 | } 37 | } 38 | 39 | extension EdgeView: Inspectable { 40 | 41 | } 42 | 43 | extension Arrow: Inspectable { 44 | 45 | } 46 | -------------------------------------------------------------------------------- /Tests/Views/GraphViewModelTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DirectedGraph 3 | 4 | class GraphViewModelTests: XCTestCase { 5 | func testInit() { 6 | let node1 = SimpleNode(id: "1", group: 3) 7 | let node2 = SimpleNode(id: "2", group: 3) 8 | let edge = SimpleEdge(source: "1", target: "2", value: 3) 9 | let graph = SimpleGraph(nodes: [node1, node2], edges: [edge]) 10 | 11 | let graphViewModel = GraphViewModel(graph) 12 | 13 | XCTAssertEqual(graphViewModel.graphNodes.count, 2) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/Views/NodeViewModelTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import DirectedGraph 3 | 4 | class NodeViewModelTests: XCTestCase { 5 | func testInit() { 6 | let node = SimpleNode(id: "2", group: 3) 7 | 8 | let nodeViewModel = NodeViewModel(node) 9 | 10 | XCTAssertEqual(nodeViewModel.id, "2") 11 | XCTAssertEqual(nodeViewModel.position, .zero) 12 | XCTAssertEqual(nodeViewModel.velocity, .zero) 13 | XCTAssertEqual(nodeViewModel.size, .zero) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/Views/NodeViewTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SwiftUI 3 | import ViewInspector 4 | @testable import DirectedGraph 5 | 6 | class NodeViewTests: XCTestCase { 7 | private let viewModel = NodeViewModel(SimpleNode(id: "2", group: 3)) 8 | 9 | func testInit() throws { 10 | let expectedText = "Test" 11 | let view = NodeView(viewModel: viewModel) { 12 | Text(expectedText) 13 | } 14 | 15 | let text = try view.inspect().view(SizeReader.self).zStack().text(0).string() 16 | 17 | XCTAssertEqual(text, expectedText) 18 | } 19 | 20 | func testPosition() throws { 21 | let expectedPosition = CGPoint(x: 1, y: 10) 22 | viewModel.position = expectedPosition 23 | let view = makeView(viewModel) 24 | 25 | let position = try view.inspect().view(SizeReader.self).position() 26 | 27 | XCTAssertEqual(position, expectedPosition) 28 | } 29 | 30 | func testDoubleTapToggleInteractive() throws { 31 | let view = makeView(viewModel) 32 | 33 | try view.callOnTapGesture() 34 | 35 | XCTAssertEqual(viewModel.interactive, true) 36 | 37 | try view.callOnTapGesture() 38 | 39 | XCTAssertEqual(viewModel.interactive, false) 40 | } 41 | 42 | func makeView(_ viewModel: NodeViewModel) -> NodeView { 43 | return NodeView(viewModel: viewModel) { 44 | Text("Test") 45 | } 46 | } 47 | } 48 | 49 | extension NodeView: Inspectable where Content == Text { 50 | func callOnTapGesture() throws { 51 | try inspect().view(SizeReader.self).callOnTapGesture() 52 | } 53 | } 54 | 55 | extension SizeReader: Inspectable where Content == Text { 56 | 57 | } 58 | --------------------------------------------------------------------------------