├── Shared
├── Assets.xcassets
│ ├── Contents.json
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ └── Contents.json
├── ShapeEditApp.swift
├── Model
│ ├── ShapeEditDocument.swift
│ ├── Graphic.swift
│ ├── Tree.swift
│ └── Graphic+Test.swift
├── Inspector
│ ├── LineWidthRowView.swift
│ ├── ColorRowView.swift
│ └── InspectorView.swift
├── CanvasView
│ ├── GridView.swift
│ ├── Utilities.swift
│ └── CanvasView.swift
├── Auxiliary
│ ├── NavigatorView.swift
│ ├── GraphicShapeView.swift
│ ├── DragInfo.swift
│ ├── LibraryView.swift
│ └── SelectionView.swift
└── ContentView.swift
├── macOS
├── macOS.entitlements
└── Info.plist
├── Tests iOS
├── Info.plist
└── Tests_iOS.swift
├── Tests macOS
├── Info.plist
└── Tests_macOS.swift
├── README.md
├── .gitignore
├── ShapeEdit.xcodeproj
├── xcshareddata
│ └── xcschemes
│ │ └── ShapeEdit (iOS).xcscheme
└── project.pbxproj
└── iOS
└── Info.plist
/Shared/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Shared/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/macOS/macOS.entitlements:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.app-sandbox
6 |
7 | com.apple.security.files.user-selected.read-write
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/Shared/ShapeEditApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShapeEditApp.swift
3 | // Shared
4 | //
5 | // Created by Dmytro Anokhin on 22/07/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct ShapeEditApp: App {
12 | var body: some Scene {
13 | DocumentGroup(newDocument: ShapeEditDocument()) { file in
14 | ContentView(document: file.$document)
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Tests iOS/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 |
22 |
23 |
--------------------------------------------------------------------------------
/Tests macOS/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 |
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ShapeEdit
2 |
3 | ShapeEdit is a showcase for [Advanced ScrollView](https://github.com/dmytro-anokhin/advanced-scrollview), inspired by WWDC sample with the same name. ShapeEdit is build in SwiftUI, with exception of the scroll view, that is `UIScrollView` or `NSScrollView` under the hood.
4 |
5 | ShapeEdit contains some shapes that can be interacted with on a fixed size canvas. You can:
6 | - Drag shapes around;
7 | - Resize shapes;
8 | - Create new shapes;
9 | - Change fill and stroke color;
10 | - On macOS scroll view will autoscroll to follow the coursor;
11 | - Zoom in/out;
12 | - Delete elements from a context menu.
13 |
14 | Note: this is just a prototype, it's not optimized for performance in any way and should be considered as a case study if you want to build something similar. You need Xcode 13 to run it. I developed it mainly for macOS, some features not working on iOS yet.
15 |
16 | 
17 |
18 |
--------------------------------------------------------------------------------
/Shared/Model/ShapeEditDocument.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ShapeEditDocument.swift
3 | // Shared
4 | //
5 | // Created by Dmytro Anokhin on 22/07/2021.
6 | //
7 |
8 | import SwiftUI
9 | import UniformTypeIdentifiers
10 |
11 | extension UTType {
12 |
13 | static let shapeEditDocument = UTType(exportedAs: "com.example.ShapeEdit.shapes")
14 | }
15 |
16 | struct ShapeEditDocument: FileDocument, Codable {
17 |
18 | var graphics: [Graphic]
19 |
20 | init(graphics: [Graphic] = Graphic.generateSample(length: 5, children: [2])) {
21 | self.graphics = graphics
22 | }
23 |
24 | static var readableContentTypes: [UTType] { [.shapeEditDocument] }
25 |
26 | init(configuration: ReadConfiguration) throws {
27 | guard let data = configuration.file.regularFileContents else {
28 | throw CocoaError(.fileReadCorruptFile)
29 | }
30 |
31 | self = try JSONDecoder().decode(Self.self, from: data)
32 | }
33 |
34 | func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
35 | let data = try JSONEncoder().encode(self)
36 | return .init(regularFileWithContents: data)
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Shared/Inspector/LineWidthRowView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LineWidthRowView.swift
3 | // LineWidthRowView
4 | //
5 | // Created by Dmytro Anokhin on 18/08/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct LineWidthRowView: View {
11 |
12 | static let formatter: Formatter = {
13 | let formatter = NumberFormatter()
14 | formatter.numberStyle = .decimal
15 | formatter.minimumFractionDigits = 1
16 | formatter.maximumFractionDigits = 1
17 |
18 | return formatter
19 | }()
20 |
21 | var title: String
22 |
23 | @Binding var value: CGFloat
24 |
25 | var step: CGFloat = 0.5
26 | var range: ClosedRange = 0.0...10.0
27 |
28 | var body: some View {
29 | let formattedValue = LineWidthRowView.formatter.string(for: value)!
30 |
31 | Stepper {
32 | Text("\(title) \(formattedValue)")
33 | } onIncrement: {
34 | value = min(value + step, range.upperBound)
35 | } onDecrement: {
36 | value = min(value + step, range.lowerBound)
37 | }
38 | }
39 | }
40 |
41 | struct LineWidthRowView_Previews: PreviewProvider {
42 | static var previews: some View {
43 | LineWidthRowView(title: "Line Width:", value: .constant(1.0))
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/Shared/CanvasView/GridView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GridView.swift
3 | // GridView
4 | //
5 | // Created by Dmytro Anokhin on 30/07/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 |
11 | struct GridView: View {
12 |
13 | var size: CGSize
14 |
15 | var length: CGFloat = 100.0
16 |
17 | var body: some View {
18 | ZStack {
19 | path(size: size, length: length * 0.25)
20 | .stroke(style: StrokeStyle(lineWidth: 0.5))
21 | .foregroundColor(Color(.displayP3, white: 0.75, opacity: 1.0))
22 |
23 | path(size: size, length: length)
24 | .stroke(style: StrokeStyle(lineWidth: 0.25))
25 | .foregroundColor(Color(.displayP3, white: 0.25, opacity: 1.0))
26 | }
27 | .background(Color.white)
28 | }
29 |
30 | func path(size: CGSize, length: CGFloat) -> Path {
31 | Path { path in
32 | var x = length
33 |
34 | while x < size.width {
35 | path.move(to: CGPoint(x: x, y: 0.0))
36 | path.addLine(to: CGPoint(x: x, y: size.height))
37 |
38 | x += length
39 | }
40 |
41 | var y = length
42 |
43 | while y < size.height {
44 | path.move(to: CGPoint(x: 0.0, y: y))
45 | path.addLine(to: CGPoint(x: size.width, y: y))
46 |
47 | y += length
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/Shared/Auxiliary/NavigatorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // NavigatorView.swift
3 | // NavigatorView
4 | //
5 | // Created by Dmytro Anokhin on 02/08/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 |
11 | struct NavigatorView: View {
12 |
13 | var graphics: [Graphic]
14 |
15 | @Binding var selection: Set
16 |
17 | var body: some View {
18 | List(selection: $selection) {
19 | // Using section breaks selection when some items are collapsed in Xcode 13.0 beta 3 (13A5192j)
20 | //Section(header: Text("Canvas")) {
21 | OutlineGroup(graphics, children: \.children) {
22 | GraphicRow($0)
23 | }
24 | //}
25 | }
26 | .listStyle(.sidebar)
27 | }
28 | }
29 |
30 | extension NavigatorView {
31 |
32 | struct GraphicRow: View {
33 |
34 | var graphic: Graphic
35 |
36 | init(_ graphic: Graphic) {
37 | self.graphic = graphic
38 | }
39 |
40 | var body: some View {
41 | HStack {
42 | GraphicShapeView(graphic: graphic)
43 | .aspectRatio(1.0, contentMode: .fit)
44 | .frame(height: 17.0)
45 | Text(graphic.name)
46 | }.padding(.leading, 8.0)
47 | }
48 | }
49 | }
50 |
51 |
52 | struct NavigatorView_Previews: PreviewProvider {
53 | static var previews: some View {
54 | NavigatorView(graphics: Graphic.smallSet, selection: .constant([]))
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/Tests iOS/Tests_iOS.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tests_iOS.swift
3 | // Tests iOS
4 | //
5 | // Created by Dmytro Anokhin on 22/07/2021.
6 | //
7 |
8 | import XCTest
9 |
10 | class Tests_iOS: XCTestCase {
11 |
12 | override func setUpWithError() throws {
13 | // Put setup code here. This method is called before the invocation of each test method in the class.
14 |
15 | // In UI tests it is usually best to stop immediately when a failure occurs.
16 | continueAfterFailure = false
17 |
18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
19 | }
20 |
21 | override func tearDownWithError() throws {
22 | // Put teardown code here. This method is called after the invocation of each test method in the class.
23 | }
24 |
25 | func testExample() throws {
26 | // UI tests must launch the application that they test.
27 | let app = XCUIApplication()
28 | app.launch()
29 |
30 | // Use recording to get started writing UI tests.
31 | // Use XCTAssert and related functions to verify your tests produce the correct results.
32 | }
33 |
34 | func testLaunchPerformance() throws {
35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
36 | // This measures how long it takes to launch your application.
37 | measure(metrics: [XCTApplicationLaunchMetric()]) {
38 | XCUIApplication().launch()
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Tests macOS/Tests_macOS.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tests_macOS.swift
3 | // Tests macOS
4 | //
5 | // Created by Dmytro Anokhin on 22/07/2021.
6 | //
7 |
8 | import XCTest
9 |
10 | class Tests_macOS: XCTestCase {
11 |
12 | override func setUpWithError() throws {
13 | // Put setup code here. This method is called before the invocation of each test method in the class.
14 |
15 | // In UI tests it is usually best to stop immediately when a failure occurs.
16 | continueAfterFailure = false
17 |
18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
19 | }
20 |
21 | override func tearDownWithError() throws {
22 | // Put teardown code here. This method is called after the invocation of each test method in the class.
23 | }
24 |
25 | func testExample() throws {
26 | // UI tests must launch the application that they test.
27 | let app = XCUIApplication()
28 | app.launch()
29 |
30 | // Use recording to get started writing UI tests.
31 | // Use XCTAssert and related functions to verify your tests produce the correct results.
32 | }
33 |
34 | func testLaunchPerformance() throws {
35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
36 | // This measures how long it takes to launch your application.
37 | measure(metrics: [XCTApplicationLaunchMetric()]) {
38 | XCUIApplication().launch()
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Shared/Auxiliary/GraphicShapeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // GraphicShapeView.swift
3 | // GraphicShapeView
4 | //
5 | // Created by Dmytro Anokhin on 02/08/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct GraphicShapeView: View {
11 |
12 | var graphic: Graphic
13 |
14 | var body: some View {
15 | switch graphic.content {
16 | case .rect:
17 | ZStack {
18 | if let fill = graphic.fill {
19 | Rectangle()
20 | .fill(fill.color)
21 | }
22 |
23 | if let stroke = graphic.stroke, stroke.lineWidth > 0.0 {
24 | Rectangle()
25 | .stroke(stroke.style.color, lineWidth: stroke.lineWidth)
26 | }
27 | }
28 |
29 | case .triangle:
30 | ZStack {
31 | if let fill = graphic.fill {
32 | Triangle()
33 | .fill(fill.color)
34 | }
35 |
36 | if let stroke = graphic.stroke, stroke.lineWidth > 0.0 {
37 | Triangle()
38 | .stroke(stroke.style.color, lineWidth: stroke.lineWidth)
39 | }
40 | }
41 |
42 | case .ellipse:
43 | ZStack {
44 | if let fill = graphic.fill {
45 | Ellipse()
46 | .fill(fill.color)
47 | }
48 |
49 | if let stroke = graphic.stroke, stroke.lineWidth > 0.0 {
50 | Ellipse()
51 | .stroke(stroke.style.color, lineWidth: stroke.lineWidth)
52 | }
53 | }
54 | }
55 | }
56 | }
57 |
58 | struct GraphicShapeView_Previews: PreviewProvider {
59 | static var previews: some View {
60 | GraphicShapeView(graphic: Graphic.smallSet.first!)
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Shared/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // Shared
4 | //
5 | // Created by Dmytro Anokhin on 22/07/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ContentView: View {
11 |
12 | @Binding var document: ShapeEditDocument
13 |
14 | @State var selection: Set = []
15 |
16 | @State private var isLibraryPresented = false
17 |
18 | var body: some View {
19 | #if os(macOS)
20 | HSplitView {
21 | NavigatorView(graphics: document.graphics, selection: $selection)
22 | .frame(width: 200.0)
23 |
24 | HSplitView {
25 | CanvasView(graphics: $document.graphics, selection: $selection)
26 | .layoutPriority(1.0)
27 | InspectorView(graphics: $document.graphics, selection: $selection)
28 | .frame(minWidth: 200.0, maxWidth: 320.0)
29 | }
30 | }
31 | .toolbar {
32 | ToolbarItemGroup(placement: .primaryAction) {
33 | Button {
34 | isLibraryPresented.toggle()
35 | } label: {
36 | Image(systemName: "square.on.circle")
37 | }
38 | .popover(isPresented: $isLibraryPresented) {
39 | LibraryView(document: $document)
40 | }
41 | }
42 | }
43 | #else
44 | CanvasView(graphics: $document.graphics, selection: $selection)
45 | .toolbar {
46 | ToolbarItemGroup(placement: .primaryAction) {
47 | Button {
48 | isLibraryPresented.toggle()
49 | } label: {
50 | Image(systemName: "square.on.circle")
51 | }
52 | .popover(isPresented: $isLibraryPresented) {
53 | LibraryView(document: $document)
54 | }
55 | }
56 | }
57 | #endif
58 | }
59 | }
60 |
61 | struct ContentView_Previews: PreviewProvider {
62 | static var previews: some View {
63 | ContentView(document: .constant(ShapeEditDocument()))
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/Shared/Model/Graphic.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Graphic.swift
3 | // ShapeEdit
4 | //
5 | // Created by Dmytro Anokhin on 22/07/2021.
6 | //
7 |
8 | import SwiftUI
9 | import Foundation
10 |
11 |
12 | struct Graphic: TreeNode, Hashable, Codable, Identifiable {
13 |
14 | var id: String
15 |
16 | var name: String
17 |
18 | var content: Content
19 |
20 | var children: [Graphic]?
21 |
22 | var fill: PaletteColor?
23 |
24 | var stroke: Graphic.Stroke?
25 |
26 | var offset: CGPoint = .zero
27 | var size: CGSize = .zero
28 |
29 | var frame: CGRect {
30 | CGRect(origin: offset, size: size)
31 | }
32 |
33 | // MARK: - Hashable
34 |
35 | func hash(into hasher: inout Hasher) {
36 | hasher.combine(id)
37 | }
38 | }
39 |
40 | extension Graphic {
41 |
42 | enum Content: Equatable, Codable, CaseIterable {
43 |
44 | case rect
45 |
46 | case triangle
47 |
48 | case ellipse
49 | }
50 | }
51 |
52 | extension Graphic {
53 |
54 | struct Stroke: Equatable, Codable {
55 |
56 | var style: PaletteColor
57 |
58 | var lineWidth: CGFloat
59 | }
60 |
61 | enum PaletteColor: Equatable, Codable, CaseIterable, Identifiable {
62 |
63 | var id: Self {
64 | self
65 | }
66 |
67 | case red
68 |
69 | case blue
70 |
71 | case green
72 |
73 | case cyan
74 |
75 | case magenta
76 |
77 | case yellow
78 |
79 | var color: Color {
80 | switch self {
81 | case .red:
82 | return Color(.displayP3, red: 235.0 / 255.0, green: 57.0 / 255.0, blue: 86.0 / 255.0, opacity: 1.0)
83 | case .blue:
84 | return Color(.displayP3, red: 39.0 / 255.0, green: 105.0 / 255.0, blue: 185.0 / 255.0, opacity: 1.0)
85 | case .green:
86 | return Color(.displayP3, red: 79.0 / 255.0, green: 174.0 / 255.0, blue: 92.0 / 255.0, opacity: 1.0)
87 | case .cyan:
88 | return Color(.displayP3, red: 86.0 / 255.0, green: 194.0 / 255.0, blue: 214.0 / 255.0, opacity: 1.0)
89 | case .magenta:
90 | return Color(.displayP3, red: 133.0 / 255.0, green: 46.0 / 255.0, blue: 233.0 / 255.0, opacity: 1.0)
91 | case .yellow:
92 | return Color(.displayP3, red: 247.0 / 255.0, green: 241.0 / 255.0, blue: 80.0 / 255.0, opacity: 1.0)
93 | }
94 | }
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/.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 |
92 | .DS_Store
--------------------------------------------------------------------------------
/macOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDocumentTypes
8 |
9 |
10 | CFBundleTypeRole
11 | Editor
12 | LSHandlerRank
13 | Default
14 | LSItemContentTypes
15 |
16 | com.example.ShapeEdit.shapes
17 |
18 | NSUbiquitousDocumentUserActivityType
19 | $(PRODUCT_BUNDLE_IDENTIFIER).example-document
20 |
21 |
22 | CFBundleExecutable
23 | $(EXECUTABLE_NAME)
24 | CFBundleIconFile
25 |
26 | CFBundleIdentifier
27 | $(PRODUCT_BUNDLE_IDENTIFIER)
28 | CFBundleInfoDictionaryVersion
29 | 6.0
30 | CFBundleName
31 | $(PRODUCT_NAME)
32 | CFBundlePackageType
33 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
34 | CFBundleShortVersionString
35 | 1.0
36 | CFBundleVersion
37 | 1
38 | LSMinimumSystemVersion
39 | $(MACOSX_DEPLOYMENT_TARGET)
40 | UTExportedTypeDeclarations
41 |
42 |
43 | UTTypeConformsTo
44 |
45 | public.data
46 | public.content
47 |
48 | UTTypeDescription
49 | ShapeEdit Document
50 | UTTypeIcons
51 |
52 | UTTypeIdentifier
53 | com.example.ShapeEdit.shapes
54 | UTTypeTagSpecification
55 |
56 | public.filename-extension
57 |
58 | shapes
59 |
60 |
61 |
62 |
63 | UTImportedTypeDeclarations
64 |
65 |
66 | UTTypeConformsTo
67 |
68 | public.data
69 | public.content
70 |
71 | UTTypeDescription
72 | ShapeEdit Document
73 | UTTypeIcons
74 |
75 | UTTypeIdentifier
76 | com.example.ShapeEdit.shapes
77 | UTTypeTagSpecification
78 |
79 | public.filename-extension
80 |
81 | shapes
82 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/Shared/CanvasView/Utilities.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Utilities.swift
3 | // ShapeEdit
4 | //
5 | // Created by Dmytro Anokhin on 23/07/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 |
11 | struct Triangle: Shape {
12 |
13 | func path(in rect: CGRect) -> Path {
14 | var path = Path()
15 | path.move(to: CGPoint(x: rect.midX, y: 0.0))
16 | path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
17 | path.addLine(to: CGPoint(x: 0.0, y: rect.maxY))
18 | path.closeSubpath()
19 |
20 | return path
21 | }
22 | }
23 |
24 | extension Graphic {
25 |
26 | var flatten: [Graphic] {
27 | var result: [Graphic] = []
28 | var queue: [Graphic] = [self]
29 |
30 | while !queue.isEmpty {
31 | let graphic = queue.removeFirst()
32 | result.append(graphic)
33 |
34 | if let children = graphic.children {
35 | queue.append(contentsOf: children)
36 | }
37 | }
38 |
39 | return result
40 | }
41 | }
42 |
43 | extension Graphic {
44 |
45 | func hitTest(_ point: CGPoint, includeChildren: Bool = true, extendBy delta: CGFloat = 0.0) -> Graphic? {
46 | if includeChildren, let children = children, let child = children.hitTest(point) {
47 | return child
48 | }
49 |
50 | return frame.insetBy(dx: -delta, dy: -delta).contains(point) ? self : nil
51 | }
52 | }
53 |
54 | extension Sequence where Element == Graphic {
55 |
56 | func hitTest(_ point: CGPoint, extendBy delta: CGFloat = 0.0) -> Graphic? {
57 | for element in self.reversed() {
58 | if let result = element.hitTest(point, extendBy: delta) {
59 | return result
60 | }
61 | }
62 |
63 | return nil
64 | }
65 | }
66 |
67 | // Operations on a tree of graphics. All operations are linear time O(n).
68 | extension Array where Element == Graphic {
69 |
70 | /// Flatten tree in an array
71 | var flatten: [Graphic] {
72 | var result: [Graphic] = []
73 | var queue: [Graphic] = self
74 |
75 | while !queue.isEmpty {
76 | let graphic = queue.removeFirst()
77 | result.append(graphic)
78 |
79 | if let children = graphic.children {
80 | queue.append(contentsOf: children)
81 | }
82 | }
83 |
84 | return result
85 | }
86 | }
87 |
88 | extension View {
89 |
90 | func frame(size: CGSize) -> some View {
91 | self.frame(width: size.width, height: size.height)
92 | }
93 |
94 | func frame(rect: CGRect) -> some View {
95 | self.frame(width: rect.width, height: rect.height)
96 | .position(x: rect.midX, y: rect.midY)
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/Shared/Inspector/ColorRowView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ColorRowView.swift
3 | // ColorRowView
4 | //
5 | // Created by Dmytro Anokhin on 18/08/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ColorRowView: View {
11 |
12 | #if os(macOS)
13 | static let itemSize = CGSize(width: 22.0, height: 22.0)
14 | static let selectionSize = CGSize(width: 24.0, height: 24.0)
15 | static let cornerRadius = 5.0
16 | #else
17 | static let itemSize = CGSize(width: 42.0, height: 42.0)
18 | static let selectionSize = CGSize(width: 44.0, height: 44.0)
19 | static let cornerRadius = 10.0
20 | #endif
21 |
22 | struct Item: Identifiable {
23 |
24 | var id: String {
25 | String(describing: paletteColor)
26 | }
27 |
28 | var paletteColor: Graphic.PaletteColor?
29 | }
30 |
31 | @Binding var selected: Graphic.PaletteColor?
32 |
33 | var body: some View {
34 | let columns = [
35 | GridItem(.adaptive(minimum: ColorRowView.itemSize.width, maximum: ColorRowView.itemSize.width))
36 | ]
37 |
38 | let items = Graphic.PaletteColor.allCases.map { Item(paletteColor: $0) } + [ Item(paletteColor: nil) ]
39 |
40 | return LazyVGrid(columns: columns) {
41 | ForEach(items) { item in
42 | ZStack {
43 | Rectangle()
44 | .fill(selected == item.paletteColor ? Color.blue : Color.clear)
45 | .cornerRadius(ColorRowView.cornerRadius)
46 | .aspectRatio(1.0, contentMode: .fit)
47 | .frame(width: ColorRowView.selectionSize.width,
48 | height: ColorRowView.selectionSize.height)
49 |
50 | if let paletteColor = item.paletteColor {
51 | Circle()
52 | .fill(paletteColor.color)
53 | .aspectRatio(1.0, contentMode: .fit)
54 | .frame(width: ColorRowView.itemSize.width,
55 | height: ColorRowView.itemSize.height)
56 | } else {
57 | Image(systemName: "slash.circle")
58 | .resizable()
59 | .font(Font.title.weight(.light))
60 | .foregroundColor(.gray)
61 | .aspectRatio(1.0, contentMode: .fit)
62 | .frame(width: ColorRowView.itemSize.width,
63 | height: ColorRowView.itemSize.height)
64 | }
65 | }
66 | .onTapGesture {
67 | selected = item.paletteColor
68 | }
69 | }
70 | }
71 | }
72 | }
73 |
74 | struct ColorRowView_Previews: PreviewProvider {
75 | static var previews: some View {
76 | ColorRowView(selected: .constant(.cyan))
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/ShapeEdit.xcodeproj/xcshareddata/xcschemes/ShapeEdit (iOS).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 |
--------------------------------------------------------------------------------
/Shared/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 | "idiom" : "mac",
95 | "scale" : "1x",
96 | "size" : "16x16"
97 | },
98 | {
99 | "idiom" : "mac",
100 | "scale" : "2x",
101 | "size" : "16x16"
102 | },
103 | {
104 | "idiom" : "mac",
105 | "scale" : "1x",
106 | "size" : "32x32"
107 | },
108 | {
109 | "idiom" : "mac",
110 | "scale" : "2x",
111 | "size" : "32x32"
112 | },
113 | {
114 | "idiom" : "mac",
115 | "scale" : "1x",
116 | "size" : "128x128"
117 | },
118 | {
119 | "idiom" : "mac",
120 | "scale" : "2x",
121 | "size" : "128x128"
122 | },
123 | {
124 | "idiom" : "mac",
125 | "scale" : "1x",
126 | "size" : "256x256"
127 | },
128 | {
129 | "idiom" : "mac",
130 | "scale" : "2x",
131 | "size" : "256x256"
132 | },
133 | {
134 | "idiom" : "mac",
135 | "scale" : "1x",
136 | "size" : "512x512"
137 | },
138 | {
139 | "idiom" : "mac",
140 | "scale" : "2x",
141 | "size" : "512x512"
142 | }
143 | ],
144 | "info" : {
145 | "author" : "xcode",
146 | "version" : 1
147 | }
148 | }
149 |
--------------------------------------------------------------------------------
/Shared/Auxiliary/DragInfo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DragInfo.swift
3 | // DragInfo
4 | //
5 | // Created by Dmytro Anokhin on 04/08/2021.
6 | //
7 |
8 | import CoreGraphics
9 |
10 |
11 | /// Drag direction, counter clockwise
12 | enum Direction: CaseIterable {
13 |
14 | case top
15 |
16 | case topLeft
17 |
18 | case left
19 |
20 | case bottomLeft
21 |
22 | case bottom
23 |
24 | case bottomRight
25 |
26 | case right
27 |
28 | case topRight
29 | }
30 |
31 |
32 | struct DragInfo {
33 |
34 | /// Translation of the drag gesture
35 | var translation: CGSize
36 |
37 | /// Direction if dragging one of selection sides
38 | var direction: Direction?
39 |
40 | func translatedFrame(_ graphic: Graphic) -> CGRect {
41 | translatedFrame(graphic.frame)
42 | }
43 |
44 | func translatedFrame(_ rect: CGRect) -> CGRect {
45 | CGRect(origin: translatedPoint(rect.origin),
46 | size: translatedSize(rect.size))
47 | }
48 |
49 | func translatedPoint(_ point: CGPoint) -> CGPoint {
50 | var point = point
51 |
52 | if let direction = direction {
53 | switch direction {
54 | case .top:
55 | point.y += translation.height
56 |
57 | case .topLeft:
58 | point.x += translation.width
59 | point.y += translation.height
60 |
61 | case .left:
62 | point.x += translation.width
63 |
64 | case .bottomLeft:
65 | point.x += translation.width
66 |
67 | case .bottom:
68 | break
69 |
70 | case .bottomRight:
71 | break
72 |
73 | case .right:
74 | break
75 |
76 | case .topRight:
77 | point.y += translation.height
78 | }
79 | } else {
80 | point.x += translation.width
81 | point.y += translation.height
82 | }
83 |
84 | return point
85 | }
86 |
87 | func translatedSize(_ size: CGSize) -> CGSize {
88 | guard let direction = direction else {
89 | return size
90 | }
91 |
92 | var size = size
93 |
94 | switch direction {
95 | case .top:
96 | size.height -= translation.height
97 |
98 | case .topLeft:
99 | size.width -= translation.width
100 | size.height -= translation.height
101 |
102 | case .left:
103 | size.width -= translation.width
104 |
105 | case .bottomLeft:
106 | size.width -= translation.width
107 | size.height += translation.height
108 |
109 | case .bottom:
110 | size.height += translation.height
111 |
112 | case .bottomRight:
113 | size.width += translation.width
114 | size.height += translation.height
115 |
116 | case .right:
117 | size.width += translation.width
118 |
119 | case .topRight:
120 | size.width += translation.width
121 | size.height -= translation.height
122 | }
123 |
124 | return size
125 | }
126 |
127 | }
128 |
--------------------------------------------------------------------------------
/Shared/Inspector/InspectorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // InspectorView.swift
3 | // InspectorView
4 | //
5 | // Created by Dmytro Anokhin on 06/08/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct InspectorView: View {
11 |
12 | @Binding var graphics: [Graphic]
13 |
14 | @Binding var selection: Set
15 |
16 | var body: some View {
17 | ScrollView {
18 | VStack(alignment: .leading) {
19 | Section(header: Text("Fill:")) {
20 | ColorRowView(selected: $state.fill)
21 | }
22 |
23 | Section(header: Text("Stroke:")) {
24 | ColorRowView(selected: $state.strokeColor)
25 | LineWidthRowView(title: "Line Width:", value: $state.strokeLineWidth)
26 | }
27 | }
28 | .padding(.all)
29 | }
30 | .onChange(of: selection) { newValue in
31 | let selected = graphics.flatten.filter { graphic in
32 | selection.contains(graphic.id)
33 | }
34 |
35 | if selected.count == 1 {
36 | let graphic = selected.first!
37 | state.update(graphic)
38 | } else {
39 | state.update(nil)
40 | }
41 | }
42 | .onReceive(state.objectWillChange) {
43 | guard !state.isUpdating else {
44 | return
45 | }
46 |
47 | updateBindings()
48 | }
49 | }
50 |
51 | @StateObject private var state = InspectorModel()
52 |
53 | private func updateBindings() {
54 | for id in selection {
55 | graphics.update(id) { graphic in
56 | graphic.fill = state.fill
57 | graphic.stroke = state.stroke
58 | }
59 | }
60 | }
61 | }
62 |
63 | final class InspectorModel: ObservableObject {
64 |
65 | @Published var fill: Graphic.PaletteColor?
66 |
67 | @Published var strokeColor: Graphic.PaletteColor?
68 |
69 | @Published var strokeLineWidth: CGFloat = 1.0
70 |
71 | var stroke: Graphic.Stroke? {
72 | get {
73 | if let strokeColor = strokeColor, strokeLineWidth > 0.0 {
74 | return .init(style: strokeColor, lineWidth: strokeLineWidth)
75 | } else {
76 | return nil
77 | }
78 | }
79 |
80 | set {
81 | strokeColor = newValue?.style
82 | strokeLineWidth = newValue?.lineWidth ?? 0.0
83 | }
84 | }
85 |
86 | init() {
87 | fill = nil
88 | strokeColor = nil
89 | }
90 |
91 | func update(_ graphic: Graphic?) {
92 | isUpdating = true
93 |
94 | fill = graphic?.fill
95 | stroke = graphic?.stroke
96 |
97 | DispatchQueue.main.async {
98 | self.isUpdating = false
99 | }
100 | }
101 |
102 | /// Calling `update(_:)` method sets this flag and resets asynchronously on the main queue after publishing update. This prevents client code from setting style on a same graphic object. And in particular, resetting styles when multiple graphic object selected.
103 | var isUpdating: Bool = false
104 | }
105 |
106 |
107 | //struct InspectorView_Previews: PreviewProvider {
108 | // static var previews: some View {
109 | // InspectorView()
110 | // }
111 | //}
112 |
--------------------------------------------------------------------------------
/Shared/Auxiliary/LibraryView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LibraryView.swift
3 | // LibraryView
4 | //
5 | // Created by Dmytro Anokhin on 05/08/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct LibraryView: View {
11 |
12 | struct Item: Identifiable {
13 |
14 | var id: Int
15 |
16 | var graphic: Graphic
17 | }
18 |
19 | static let graphicSize = CGSize(width: 64.0, height: 64.0)
20 |
21 | static let itemPadding = 20.0
22 |
23 | var items: [Item] = [
24 | Item(id: 1,
25 | graphic: Graphic(id: "rect",
26 | name: "Rectangle",
27 | content: .rect,
28 | children: nil,
29 | fill: .cyan,
30 | stroke: nil,
31 | offset: .zero,
32 | size: LibraryView.graphicSize)),
33 |
34 | Item(id: 2,
35 | graphic: Graphic(id: "triangle",
36 | name: "Triangle",
37 | content: .triangle,
38 | children: nil,
39 | fill: .magenta,
40 | stroke: nil,
41 | offset: .zero,
42 | size: LibraryView.graphicSize)),
43 |
44 | Item(id: 3,
45 | graphic: Graphic(id: "ellipse",
46 | name: "Ellipse",
47 | content: .ellipse,
48 | children: nil,
49 | fill: .yellow,
50 | stroke: nil,
51 | offset: .zero,
52 | size: LibraryView.graphicSize))
53 | ]
54 |
55 | @Binding var document: ShapeEditDocument
56 |
57 | @State var selectedItem: Int? = nil
58 |
59 | var body: some View {
60 | let minWidth = LibraryView.graphicSize.width + LibraryView.itemPadding * 2.0
61 |
62 | let columns = [
63 | GridItem(.flexible(minimum: minWidth, maximum: .infinity)),
64 | GridItem(.flexible(minimum: minWidth, maximum: .infinity))
65 | ]
66 |
67 | LazyVGrid(columns: columns) {
68 | Section(header: Text("Library")) {
69 | ForEach(items) { item in
70 | ZStack {
71 | Rectangle()
72 | .fill(Color(.displayP3, white: 1.0, opacity: 0.1))
73 | .cornerRadius(10.0)
74 | .aspectRatio(1.0, contentMode: .fit)
75 |
76 | GraphicShapeView(graphic: item.graphic)
77 | .aspectRatio(1.0, contentMode: .fit)
78 | .padding(LibraryView.itemPadding)
79 | }
80 | .onTapGesture {
81 | var graphic = item.graphic
82 | graphic.id = UUID().uuidString
83 | graphic.size = CGSize(width: 100.0, height: 100.0)
84 | document.graphics.append(graphic)
85 | }
86 | }
87 | }
88 | }
89 | .padding(.all)
90 | }
91 | }
92 |
93 | struct LibraryView_Previews: PreviewProvider {
94 | static var previews: some View {
95 | LibraryView(document: .constant(ShapeEditDocument()))
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/iOS/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDocumentTypes
8 |
9 |
10 | CFBundleTypeRole
11 | Editor
12 | LSHandlerRank
13 | Default
14 | LSItemContentTypes
15 |
16 | com.example.ShapeEdit.shapes
17 |
18 | NSUbiquitousDocumentUserActivityType
19 | $(PRODUCT_BUNDLE_IDENTIFIER).example-document
20 |
21 |
22 | CFBundleExecutable
23 | $(EXECUTABLE_NAME)
24 | CFBundleIdentifier
25 | $(PRODUCT_BUNDLE_IDENTIFIER)
26 | CFBundleInfoDictionaryVersion
27 | 6.0
28 | CFBundleName
29 | $(PRODUCT_NAME)
30 | CFBundlePackageType
31 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
32 | CFBundleShortVersionString
33 | 1.0
34 | CFBundleVersion
35 | 1
36 | LSRequiresIPhoneOS
37 |
38 | LSSupportsOpeningDocumentsInPlace
39 |
40 | UIApplicationSceneManifest
41 |
42 | UIApplicationSupportsMultipleScenes
43 |
44 |
45 | UIApplicationSupportsIndirectInputEvents
46 |
47 | UILaunchScreen
48 |
49 | UIRequiredDeviceCapabilities
50 |
51 | armv7
52 |
53 | UISupportedInterfaceOrientations
54 |
55 | UIInterfaceOrientationPortrait
56 | UIInterfaceOrientationLandscapeLeft
57 | UIInterfaceOrientationLandscapeRight
58 |
59 | UISupportedInterfaceOrientations~ipad
60 |
61 | UIInterfaceOrientationPortrait
62 | UIInterfaceOrientationPortraitUpsideDown
63 | UIInterfaceOrientationLandscapeLeft
64 | UIInterfaceOrientationLandscapeRight
65 |
66 | UISupportsDocumentBrowser
67 |
68 | UTExportedTypeDeclarations
69 |
70 |
71 | UTTypeConformsTo
72 |
73 | public.data
74 | public.content
75 |
76 | UTTypeDescription
77 | ShapeEdit Document
78 | UTTypeIcons
79 |
80 | UTTypeIdentifier
81 | com.example.ShapeEdit.shapes
82 | UTTypeTagSpecification
83 |
84 | public.filename-extension
85 |
86 | shapes
87 |
88 |
89 |
90 |
91 | UTImportedTypeDeclarations
92 |
93 |
94 | UTTypeConformsTo
95 |
96 | public.data
97 | public.content
98 |
99 | UTTypeDescription
100 | ShapeEdit Document
101 | UTTypeIcons
102 |
103 | UTTypeIdentifier
104 | com.example.ShapeEdit.shapes
105 | UTTypeTagSpecification
106 |
107 | public.filename-extension
108 |
109 | shapes
110 |
111 |
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/Shared/Model/Tree.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Tree.swift
3 | // Tree
4 | //
5 | // Created by Dmytro Anokhin on 16/08/2021.
6 | //
7 |
8 | protocol TreeNode {
9 |
10 | associatedtype Child: TreeNode
11 |
12 | var children: [Child]? { get set }
13 | }
14 |
15 |
16 | extension Array where Element: TreeNode, Element: Identifiable, Element.Child == Element {
17 |
18 | // MARK: - Update
19 |
20 | /// Updates the node that matches the id
21 | ///
22 | /// - Parameters:
23 | /// - id: The id of the node to update;
24 | /// - change: The closure that encapsulate update logic for the node.
25 | ///
26 | /// This is recursive operation in O(n) time.
27 | mutating func update(_ id: Element.ID, change: (_ element: inout Element) -> Void) {
28 | _update(id, change: change)
29 | }
30 |
31 | /// Helper for `update` that returns `true` if the node that matches given id was found.
32 | @discardableResult
33 | private mutating func _update(_ id: Element.ID, change: (_ element: inout Element) -> Void) -> Bool {
34 | for index in 0.. Bool {
76 | var indexToRemove: Int?
77 |
78 | for index in 0.. Bool) rethrows -> [Element] {
101 | try filter { element in
102 | if try isIncluded(element) {
103 | return true
104 | }
105 |
106 | if let filteredChildren = try element.children?.recursiveFilter(isIncluded) {
107 | return !filteredChildren.isEmpty
108 | }
109 |
110 | return false
111 | }
112 | }
113 | //
114 | // /// Flatten tree in an array
115 | // func flatten() -> [Element] {
116 | // var result: [Element] = []
117 | // var queue: [Element] = self
118 | //
119 | // while !queue.isEmpty {
120 | // let element = queue.removeFirst()
121 | // result.append(element)
122 | //
123 | // if let children = element.children {
124 | // queue.append(contentsOf: children)
125 | // }
126 | // }
127 | //
128 | // return result
129 | // }
130 | }
131 |
--------------------------------------------------------------------------------
/Shared/Model/Graphic+Test.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Graphic+Test.swift
3 | // Graphic+Test
4 | //
5 | // Created by Dmytro Anokhin on 08/08/2021.
6 | //
7 |
8 | import Foundation
9 | import CoreGraphics
10 |
11 | extension Graphic {
12 |
13 | static var smallSet: [Graphic] {
14 | [
15 | Graphic(id: UUID().uuidString,
16 | name: "Rectangle",
17 | content: .rect,
18 | children: [
19 | Graphic(id: UUID().uuidString,
20 | name: "Rectangle",
21 | content: .rect,
22 | children: nil,
23 | fill: .red,
24 | stroke: nil,
25 | offset: CGPoint(x: 425.0, y: 125.0),
26 | size: CGSize(width: 50.0, height: 50.0)),
27 | Graphic(id: UUID().uuidString,
28 | name: "Triangle",
29 | content: .triangle,
30 | children: nil,
31 | fill: .green,
32 | stroke: nil,
33 | offset: CGPoint(x: 450.0, y: 110.0),
34 | size: CGSize(width: 50.0, height: 50.0)),
35 | Graphic(id: UUID().uuidString,
36 | name: "Ellipse",
37 | content: .ellipse,
38 | children: nil,
39 | fill: .blue,
40 | stroke: nil,
41 | offset: CGPoint(x: 400.0, y: 100.0),
42 | size: CGSize(width: 50.0, height: 50.0))
43 | ],
44 | fill: .cyan,
45 | offset: CGPoint(x: 400.0, y: 100.0),
46 | size: CGSize(width: 200.0, height: 200.0)),
47 | Graphic(id: UUID().uuidString,
48 | name: "Triangle",
49 | content: .triangle,
50 | children: nil,
51 | fill: .magenta,
52 | stroke: nil,
53 | offset: CGPoint(x: 550.0, y: 200.0),
54 | size: CGSize(width: 300.0, height: 200.0)),
55 | Graphic(id: UUID().uuidString,
56 | name: "Ellipse",
57 | content: .ellipse,
58 | children: nil,
59 | fill: .yellow,
60 | stroke: nil,
61 | offset: CGPoint(x: 300.0, y: 300.0),
62 | size: CGSize(width: 250.0, height: 250.0))
63 | ]
64 | }
65 |
66 | static func generateSample(length: Int, children childrenPattern: [Int]) -> [Graphic] {
67 | var result: [Graphic] = []
68 |
69 | let minSize = CGSize(width: 100.0, height: 100.0)
70 | let maxSize = CGSize(width: 400.0, height: 400.0)
71 |
72 | let minOffset = CGPoint(x: 0.0, y: 0.0)
73 | let maxOffset = CGPoint(x: 1920.0, y: 1080.0)
74 |
75 | for _ in 0..
17 |
18 | @State var canvasSize = CGSize(width: 1920.0, height: 1080.0)
19 |
20 | @State private var dragInfo: DragInfo? = nil
21 |
22 | var body: some View {
23 | AdvancedScrollView(magnification: Magnification(range: 1.0...4.0, initialValue: 1.0, isRelative: false)) { proxy in
24 | canvas
25 | .frame(width: canvasSize.width, height: canvasSize.height)
26 | }.onTapContentGesture { location, proxy in
27 | selection.removeAll()
28 |
29 | if let graphic = graphics.hitTest(location, extendBy: 0.0) {
30 | //selection.formUnion(graphic.flatten.map({ $0.id }))
31 | selection.insert(graphic.id)
32 | }
33 | }
34 | .onDragContentGesture { phase, location, translation, proxy in
35 | switch phase {
36 | case .possible:
37 | guard !selection.isEmpty else {
38 | return false
39 | }
40 |
41 | let selected = graphics.recursiveFilter {
42 | selection.contains($0.id)
43 | && $0.hitTest(location, includeChildren: false, extendBy: SelectionProxy.radius) != nil
44 | }.flatten
45 |
46 | return !selected.isEmpty
47 |
48 | case .began:
49 | guard !selection.isEmpty else {
50 | return false
51 | }
52 |
53 | dragInfo = DragInfo(translation: translation, direction: nil)
54 |
55 | let selected = graphics.recursiveFilter {
56 | selection.contains($0.id)
57 | && $0.hitTest(location, includeChildren: false, extendBy: SelectionProxy.radius) != nil
58 | }.flatten
59 |
60 | for graphic in selected {
61 | let selectionProxy = SelectionProxy(graphic: graphic)
62 |
63 | if let direction = selectionProxy.hitTest(location) {
64 | dragInfo = DragInfo(translation: translation, direction: direction)
65 | break
66 | }
67 | }
68 |
69 | case .changed:
70 | dragInfo?.translation = translation
71 |
72 | case .cancelled:
73 | dragInfo = nil
74 |
75 | case .ended:
76 | if let dragInfo = dragInfo {
77 | for id in selection {
78 | graphics.update(id) { graphic in
79 | graphic.offset = dragInfo.translatedPoint(graphic.offset)
80 | graphic.size = dragInfo.translatedSize(graphic.size)
81 | }
82 | }
83 | }
84 |
85 | dragInfo = nil
86 | }
87 |
88 | return true
89 | }
90 | }
91 |
92 | private var selections: [SelectionProxy] {
93 | graphics.flatten.compactMap { graphic in
94 | if selection.contains(graphic.id) {
95 | return SelectionProxy(graphic: graphic)
96 | } else {
97 | return nil
98 | }
99 | }
100 | }
101 |
102 | @ViewBuilder var canvas: some View {
103 | ZStack(alignment: .topLeading) {
104 |
105 | // Grid
106 |
107 | GridView(size: canvasSize)
108 |
109 | // Graphics
110 |
111 | ForEach($graphics) { graphic in
112 | makeTreeView(root: graphic.wrappedValue)
113 | }
114 |
115 | // Selection overlay
116 | ForEach(selections) { proxy in
117 | if let dragInfo = dragInfo, selection.contains(proxy.id) {
118 | SelectionView(proxy: proxy)
119 | .frame(rect: dragInfo.translatedFrame(proxy.selectionFrame))
120 | } else {
121 | SelectionView(proxy: proxy)
122 | .frame(rect: proxy.selectionFrame)
123 | }
124 | }
125 | }
126 | }
127 |
128 | @ViewBuilder func makeTreeView(root: Graphic) -> some View {
129 | ForEach(root.flatten) { node in
130 | makeView(node)
131 | .contextMenu {
132 | Button {
133 | graphics.remove(node)
134 | } label: {
135 | Text("Delete")
136 | }
137 | }
138 | }
139 | }
140 |
141 | @ViewBuilder func makeView(_ graphic: Graphic) -> some View {
142 | if let dragInfo = dragInfo, selection.contains(graphic.id) {
143 | GraphicShapeView(graphic: graphic)
144 | .frame(rect: dragInfo.translatedFrame(graphic))
145 | } else {
146 | GraphicShapeView(graphic: graphic)
147 | .frame(rect: graphic.frame)
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/Shared/Auxiliary/SelectionView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SelectionView.swift
3 | // SelectionView
4 | //
5 | // Created by Dmytro Anokhin on 03/08/2021.
6 | //
7 |
8 | import SwiftUI
9 |
10 |
11 | struct SelectionProxy: Identifiable {
12 |
13 | static let radius: CGFloat = 5.0
14 |
15 | /// Used for hit test
16 | static let extendedRadius: CGFloat = 15.0
17 |
18 | var id: String
19 |
20 | var offset: CGPoint
21 |
22 | var size: CGSize
23 |
24 | init(graphic: Graphic) {
25 | self.id = graphic.id
26 | self.offset = graphic.offset
27 | self.size = graphic.size
28 | }
29 |
30 | var graphicBounds: CGRect {
31 | CGRect(origin: .zero, size: size)
32 | }
33 |
34 | var graphicFrame: CGRect {
35 | CGRect(origin: offset, size: size)
36 | }
37 |
38 | var selectionBounds: CGRect {
39 | CGRect(origin: .zero, size: size)
40 | .insetBy(dx: -SelectionProxy.radius, dy: -SelectionProxy.radius)
41 | }
42 |
43 | var selectionFrame: CGRect {
44 | CGRect(origin: offset, size: size)
45 | .insetBy(dx: -SelectionProxy.radius, dy: -SelectionProxy.radius)
46 | }
47 |
48 | var position: CGPoint {
49 | graphicFrame.origin
50 | }
51 |
52 | var selectionPosition: CGPoint {
53 | selectionFrame.origin
54 | }
55 |
56 | func rect(direction: Direction) -> CGRect {
57 | rect(direction: direction, in: graphicBounds)
58 | }
59 |
60 | func rect(direction: Direction, in bounds: CGRect) -> CGRect {
61 |
62 | let size = CGSize(width: SelectionProxy.radius * 2.0, height: SelectionProxy.radius * 2.0)
63 | let origin: CGPoint
64 |
65 | switch direction {
66 | case .top:
67 | origin = CGPoint(x: bounds.midX - SelectionProxy.radius, y: bounds.minY - SelectionProxy.radius)
68 | case .topLeft:
69 | origin = CGPoint(x: bounds.minX - SelectionProxy.radius, y: bounds.minY - SelectionProxy.radius)
70 | case .left:
71 | origin = CGPoint(x: bounds.minX - SelectionProxy.radius, y: bounds.midY - SelectionProxy.radius)
72 | case .bottomLeft:
73 | origin = CGPoint(x: bounds.minX - SelectionProxy.radius, y: bounds.maxY - SelectionProxy.radius)
74 | case .bottom:
75 | origin = CGPoint(x: bounds.midX - SelectionProxy.radius, y: bounds.maxY - SelectionProxy.radius)
76 | case .bottomRight:
77 | origin = CGPoint(x: bounds.maxX - SelectionProxy.radius, y: bounds.maxY - SelectionProxy.radius)
78 | case .right:
79 | origin = CGPoint(x: bounds.maxX - SelectionProxy.radius, y: bounds.midY - SelectionProxy.radius)
80 | case .topRight:
81 | origin = CGPoint(x: bounds.maxX - SelectionProxy.radius, y: bounds.minY - SelectionProxy.radius)
82 | }
83 |
84 | return CGRect(origin: origin, size: size)
85 | }
86 |
87 | func hitTest(_ location: CGPoint) -> Direction? {
88 |
89 | // Location in local coordinates
90 | let localLocation = CGPoint(x: location.x - selectionPosition.x,
91 | y: location.y - selectionPosition.y)
92 |
93 | for direction in Direction.allCases {
94 | let rect = rect(direction: direction)
95 | .insetBy(dx: -SelectionProxy.extendedRadius,
96 | dy: -SelectionProxy.extendedRadius)
97 |
98 | if rect.contains(localLocation) {
99 | return direction
100 | }
101 | }
102 |
103 | return nil
104 | }
105 | }
106 |
107 | struct SelectionView: View {
108 |
109 | var proxy: SelectionProxy
110 |
111 | init(proxy: SelectionProxy) {
112 | self.proxy = proxy
113 | }
114 |
115 | var body: some View {
116 | ZStack(alignment: .center) {
117 | SelectionBorder()
118 | .stroke(style: StrokeStyle(lineWidth: 1, dash: [5]))
119 | .foregroundColor(Color.blue)
120 |
121 | SelectionControls(proxy: proxy)
122 | .fill(Color.white)
123 |
124 | SelectionControls(proxy: proxy)
125 | .stroke(Color.blue)
126 | }
127 | }
128 | }
129 |
130 | struct SelectionBorder: Shape {
131 |
132 | private struct Segment {
133 |
134 | /// Point to connect with previous segment
135 | var from: CGPoint
136 |
137 | /// Point to connect with next segment
138 | var to: CGPoint
139 | }
140 |
141 | func path(in rect: CGRect) -> Path {
142 |
143 | let diameter = SelectionProxy.radius * 2.0
144 |
145 | let segments: [Segment] = [
146 | // Top-Left to Top-Right
147 | Segment(from: CGPoint(x: rect.minX + diameter, y: rect.minY + SelectionProxy.radius),
148 | to: CGPoint(x: rect.midX - SelectionProxy.radius, y: rect.minY + SelectionProxy.radius)),
149 |
150 | Segment(from: CGPoint(x: rect.midX + SelectionProxy.radius, y: rect.minY + SelectionProxy.radius),
151 | to: CGPoint(x: rect.maxX - diameter, y: rect.minY + SelectionProxy.radius)),
152 |
153 | // Top-Right to Bottom-Left
154 | Segment(from: CGPoint(x: rect.maxX - SelectionProxy.radius, y: rect.minY + diameter),
155 | to: CGPoint(x: rect.maxX - SelectionProxy.radius, y: rect.midY - SelectionProxy.radius)),
156 |
157 | Segment(from: CGPoint(x: rect.maxX - SelectionProxy.radius, y: rect.midY + SelectionProxy.radius),
158 | to: CGPoint(x: rect.maxX - SelectionProxy.radius, y: rect.maxY - diameter)),
159 |
160 | // Bottom-Right to Bottom-Left
161 | Segment(from: CGPoint(x: rect.maxX - diameter, y: rect.maxY - SelectionProxy.radius),
162 | to: CGPoint(x: rect.midX + SelectionProxy.radius, y: rect.maxY - SelectionProxy.radius)),
163 |
164 | Segment(from: CGPoint(x: rect.midX - SelectionProxy.radius, y: rect.maxY - SelectionProxy.radius),
165 | to: CGPoint(x: rect.minX + diameter, y: rect.maxY - SelectionProxy.radius)),
166 |
167 | // Bottom-Left to Top-Left
168 | Segment(from: CGPoint(x: rect.minX + SelectionProxy.radius, y: rect.maxY - diameter),
169 | to: CGPoint(x: rect.minX + SelectionProxy.radius, y: rect.midY + SelectionProxy.radius)),
170 |
171 | Segment(from: CGPoint(x: rect.minX + SelectionProxy.radius, y: rect.midY - SelectionProxy.radius),
172 | to: CGPoint(x: rect.minX + SelectionProxy.radius, y: rect.minY + diameter))
173 | ]
174 |
175 | var path = Path()
176 |
177 | for segment in segments {
178 | path.move(to: segment.from)
179 | path.addLine(to: segment.to)
180 | }
181 |
182 | return path
183 | }
184 | }
185 |
186 | struct SelectionControls: Shape {
187 |
188 | var proxy: SelectionProxy
189 |
190 | func path(in rect: CGRect) -> Path {
191 |
192 | var path = Path()
193 |
194 | let controls = Direction.allCases.map {
195 | proxy.rect(direction: $0,
196 | in: rect.insetBy(dx: SelectionProxy.radius, dy: SelectionProxy.radius))
197 | }
198 |
199 | for control in controls {
200 | path.addEllipse(in: control)
201 | }
202 |
203 | return path
204 | }
205 | }
206 |
207 | struct SelectionView_Previews: PreviewProvider {
208 | static var previews: some View {
209 | let graphic = Graphic(id: "",
210 | name: "",
211 | content: .ellipse,
212 | children: nil,
213 | fill: .yellow,
214 | offset: CGPoint(x: 100.0, y: 100.0),
215 | size: CGSize(width: 100.0, height: 150.0))
216 |
217 | let proxy = SelectionProxy(graphic: graphic)
218 |
219 | ZStack(alignment: .topLeading) {
220 | GraphicShapeView(graphic: graphic)
221 | .frame(width: proxy.graphicBounds.width,
222 | height: proxy.graphicBounds.height)
223 | .position(x: proxy.position.x + proxy.graphicBounds.width * 0.5,
224 | y: proxy.position.y + proxy.graphicBounds.height * 0.5)
225 |
226 | SelectionView(proxy: proxy)
227 | .frame(width: proxy.selectionBounds.width,
228 | height: proxy.selectionBounds.height)
229 | .position(x: proxy.selectionPosition.x + proxy.selectionFrame.width * 0.5,
230 | y: proxy.selectionPosition.y + proxy.selectionFrame.height * 0.5)
231 | }
232 | .frame(width: 320.0, height: 480.0)
233 | .offset(x: 0.0, y: 0.0)
234 | .background(Color.gray)
235 |
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/ShapeEdit.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 52;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | F41779E926BFA69000C43EBF /* Graphic+Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41779E826BFA69000C43EBF /* Graphic+Test.swift */; };
11 | F41779EA26BFA69000C43EBF /* Graphic+Test.swift in Sources */ = {isa = PBXBuildFile; fileRef = F41779E826BFA69000C43EBF /* Graphic+Test.swift */; };
12 | F428FBB626BD99A300C07179 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F428FBB526BD99A300C07179 /* InspectorView.swift */; };
13 | F428FBB726BD99A400C07179 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F428FBB526BD99A300C07179 /* InspectorView.swift */; };
14 | F42E979426BC43FC001C37C2 /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42E979326BC43FC001C37C2 /* LibraryView.swift */; };
15 | F42E979526BC43FC001C37C2 /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F42E979326BC43FC001C37C2 /* LibraryView.swift */; };
16 | F44A417126CD8365007EF5B6 /* ColorRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44A417026CD8365007EF5B6 /* ColorRowView.swift */; };
17 | F44A417226CD8365007EF5B6 /* ColorRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44A417026CD8365007EF5B6 /* ColorRowView.swift */; };
18 | F44A417426CD864E007EF5B6 /* LineWidthRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44A417326CD864B007EF5B6 /* LineWidthRowView.swift */; };
19 | F44A417526CD864E007EF5B6 /* LineWidthRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44A417326CD864B007EF5B6 /* LineWidthRowView.swift */; };
20 | F44D53FF26AA040000A757FB /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44D53FE26AA040000A757FB /* Tests_iOS.swift */; };
21 | F44D540A26AA040000A757FB /* Tests_macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44D540926AA040000A757FB /* Tests_macOS.swift */; };
22 | F44D540C26AA040000A757FB /* ShapeEditApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44D53E226AA03FE00A757FB /* ShapeEditApp.swift */; };
23 | F44D540D26AA040000A757FB /* ShapeEditApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44D53E226AA03FE00A757FB /* ShapeEditApp.swift */; };
24 | F44D540E26AA040000A757FB /* ShapeEditDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44D53E326AA03FE00A757FB /* ShapeEditDocument.swift */; };
25 | F44D540F26AA040000A757FB /* ShapeEditDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44D53E326AA03FE00A757FB /* ShapeEditDocument.swift */; };
26 | F44D541026AA040000A757FB /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44D53E426AA03FE00A757FB /* ContentView.swift */; };
27 | F44D541126AA040000A757FB /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44D53E426AA03FE00A757FB /* ContentView.swift */; };
28 | F44D541226AA040000A757FB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F44D53E526AA040000A757FB /* Assets.xcassets */; };
29 | F44D541326AA040000A757FB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F44D53E526AA040000A757FB /* Assets.xcassets */; };
30 | F44D542326AA0A2F00A757FB /* Graphic.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44D542226AA0A2E00A757FB /* Graphic.swift */; };
31 | F44D542426AA0A2F00A757FB /* Graphic.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44D542226AA0A2E00A757FB /* Graphic.swift */; };
32 | F44D542A26AA95FE00A757FB /* CanvasView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44D542926AA95FE00A757FB /* CanvasView.swift */; };
33 | F44D542B26AA95FE00A757FB /* CanvasView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44D542926AA95FE00A757FB /* CanvasView.swift */; };
34 | F44D542E26AA987600A757FB /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44D542D26AA987600A757FB /* Utilities.swift */; };
35 | F44D542F26AA987600A757FB /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = F44D542D26AA987600A757FB /* Utilities.swift */; };
36 | F4536CC726B907A400FCD6BA /* SelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4536CC626B907A400FCD6BA /* SelectionView.swift */; };
37 | F4536CC826B907A400FCD6BA /* SelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4536CC626B907A400FCD6BA /* SelectionView.swift */; };
38 | F457A1AE26CADCBF003464CA /* Tree.swift in Sources */ = {isa = PBXBuildFile; fileRef = F457A1AD26CADCBF003464CA /* Tree.swift */; };
39 | F457A1AF26CADCC0003464CA /* Tree.swift in Sources */ = {isa = PBXBuildFile; fileRef = F457A1AD26CADCBF003464CA /* Tree.swift */; };
40 | F47A244F26BAE8D500977136 /* DragInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F47A244E26BAE8D500977136 /* DragInfo.swift */; };
41 | F47A245026BAE8D500977136 /* DragInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = F47A244E26BAE8D500977136 /* DragInfo.swift */; };
42 | F4ED8A1626B46BEF004E7307 /* AdvancedScrollView in Frameworks */ = {isa = PBXBuildFile; productRef = F4ED8A1526B46BEF004E7307 /* AdvancedScrollView */; };
43 | F4ED8A1926B46C02004E7307 /* AdvancedScrollView in Frameworks */ = {isa = PBXBuildFile; productRef = F4ED8A1826B46C02004E7307 /* AdvancedScrollView */; };
44 | F4ED8A1B26B48916004E7307 /* GridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ED8A1A26B48916004E7307 /* GridView.swift */; };
45 | F4ED8A1C26B48916004E7307 /* GridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ED8A1A26B48916004E7307 /* GridView.swift */; };
46 | F4ED8A2026B80123004E7307 /* NavigatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ED8A1F26B80123004E7307 /* NavigatorView.swift */; };
47 | F4ED8A2126B80123004E7307 /* NavigatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ED8A1F26B80123004E7307 /* NavigatorView.swift */; };
48 | F4ED8A2326B8306E004E7307 /* GraphicShapeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ED8A2226B8306E004E7307 /* GraphicShapeView.swift */; };
49 | F4ED8A2426B8306E004E7307 /* GraphicShapeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F4ED8A2226B8306E004E7307 /* GraphicShapeView.swift */; };
50 | /* End PBXBuildFile section */
51 |
52 | /* Begin PBXContainerItemProxy section */
53 | F44D53FB26AA040000A757FB /* PBXContainerItemProxy */ = {
54 | isa = PBXContainerItemProxy;
55 | containerPortal = F44D53DD26AA03FD00A757FB /* Project object */;
56 | proxyType = 1;
57 | remoteGlobalIDString = F44D53E926AA040000A757FB;
58 | remoteInfo = "ShapeEdit (iOS)";
59 | };
60 | F44D540626AA040000A757FB /* PBXContainerItemProxy */ = {
61 | isa = PBXContainerItemProxy;
62 | containerPortal = F44D53DD26AA03FD00A757FB /* Project object */;
63 | proxyType = 1;
64 | remoteGlobalIDString = F44D53F126AA040000A757FB;
65 | remoteInfo = "ShapeEdit (macOS)";
66 | };
67 | /* End PBXContainerItemProxy section */
68 |
69 | /* Begin PBXFileReference section */
70 | F41779E826BFA69000C43EBF /* Graphic+Test.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Graphic+Test.swift"; sourceTree = ""; };
71 | F428FBB526BD99A300C07179 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = ""; };
72 | F42E979326BC43FC001C37C2 /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = ""; };
73 | F44A417026CD8365007EF5B6 /* ColorRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorRowView.swift; sourceTree = ""; };
74 | F44A417326CD864B007EF5B6 /* LineWidthRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineWidthRowView.swift; sourceTree = ""; };
75 | F44D53E226AA03FE00A757FB /* ShapeEditApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapeEditApp.swift; sourceTree = ""; };
76 | F44D53E326AA03FE00A757FB /* ShapeEditDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShapeEditDocument.swift; sourceTree = ""; };
77 | F44D53E426AA03FE00A757FB /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
78 | F44D53E526AA040000A757FB /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
79 | F44D53EA26AA040000A757FB /* ShapeEdit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ShapeEdit.app; sourceTree = BUILT_PRODUCTS_DIR; };
80 | F44D53ED26AA040000A757FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
81 | F44D53F226AA040000A757FB /* ShapeEdit.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ShapeEdit.app; sourceTree = BUILT_PRODUCTS_DIR; };
82 | F44D53F426AA040000A757FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
83 | F44D53F526AA040000A757FB /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; };
84 | F44D53FA26AA040000A757FB /* Tests iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
85 | F44D53FE26AA040000A757FB /* Tests_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_iOS.swift; sourceTree = ""; };
86 | F44D540026AA040000A757FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
87 | F44D540526AA040000A757FB /* Tests macOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "Tests macOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
88 | F44D540926AA040000A757FB /* Tests_macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tests_macOS.swift; sourceTree = ""; };
89 | F44D540B26AA040000A757FB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
90 | F44D542226AA0A2E00A757FB /* Graphic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Graphic.swift; sourceTree = ""; };
91 | F44D542926AA95FE00A757FB /* CanvasView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CanvasView.swift; sourceTree = ""; };
92 | F44D542D26AA987600A757FB /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; };
93 | F4536CC626B907A400FCD6BA /* SelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionView.swift; sourceTree = ""; };
94 | F457A1AD26CADCBF003464CA /* Tree.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tree.swift; sourceTree = ""; };
95 | F47A244E26BAE8D500977136 /* DragInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DragInfo.swift; sourceTree = ""; };
96 | F4ED8A1A26B48916004E7307 /* GridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GridView.swift; sourceTree = ""; };
97 | F4ED8A1F26B80123004E7307 /* NavigatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigatorView.swift; sourceTree = ""; };
98 | F4ED8A2226B8306E004E7307 /* GraphicShapeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphicShapeView.swift; sourceTree = ""; };
99 | /* End PBXFileReference section */
100 |
101 | /* Begin PBXFrameworksBuildPhase section */
102 | F44D53E726AA040000A757FB /* Frameworks */ = {
103 | isa = PBXFrameworksBuildPhase;
104 | buildActionMask = 2147483647;
105 | files = (
106 | F4ED8A1626B46BEF004E7307 /* AdvancedScrollView in Frameworks */,
107 | );
108 | runOnlyForDeploymentPostprocessing = 0;
109 | };
110 | F44D53EF26AA040000A757FB /* Frameworks */ = {
111 | isa = PBXFrameworksBuildPhase;
112 | buildActionMask = 2147483647;
113 | files = (
114 | F4ED8A1926B46C02004E7307 /* AdvancedScrollView in Frameworks */,
115 | );
116 | runOnlyForDeploymentPostprocessing = 0;
117 | };
118 | F44D53F726AA040000A757FB /* Frameworks */ = {
119 | isa = PBXFrameworksBuildPhase;
120 | buildActionMask = 2147483647;
121 | files = (
122 | );
123 | runOnlyForDeploymentPostprocessing = 0;
124 | };
125 | F44D540226AA040000A757FB /* Frameworks */ = {
126 | isa = PBXFrameworksBuildPhase;
127 | buildActionMask = 2147483647;
128 | files = (
129 | );
130 | runOnlyForDeploymentPostprocessing = 0;
131 | };
132 | /* End PBXFrameworksBuildPhase section */
133 |
134 | /* Begin PBXGroup section */
135 | F428FBB426BD996400C07179 /* Inspector */ = {
136 | isa = PBXGroup;
137 | children = (
138 | F428FBB526BD99A300C07179 /* InspectorView.swift */,
139 | F44A417026CD8365007EF5B6 /* ColorRowView.swift */,
140 | F44A417326CD864B007EF5B6 /* LineWidthRowView.swift */,
141 | );
142 | path = Inspector;
143 | sourceTree = "";
144 | };
145 | F44D53DC26AA03FD00A757FB = {
146 | isa = PBXGroup;
147 | children = (
148 | F44D53E126AA03FE00A757FB /* Shared */,
149 | F44D53EC26AA040000A757FB /* iOS */,
150 | F44D53F326AA040000A757FB /* macOS */,
151 | F44D53FD26AA040000A757FB /* Tests iOS */,
152 | F44D540826AA040000A757FB /* Tests macOS */,
153 | F44D53EB26AA040000A757FB /* Products */,
154 | F4ED8A1726B46C02004E7307 /* Frameworks */,
155 | );
156 | sourceTree = "";
157 | };
158 | F44D53E126AA03FE00A757FB /* Shared */ = {
159 | isa = PBXGroup;
160 | children = (
161 | F428FBB426BD996400C07179 /* Inspector */,
162 | F4ED8A1E26B800C5004E7307 /* Auxiliary */,
163 | F44D542C26AA985F00A757FB /* Model */,
164 | F44D542826AA95F000A757FB /* CanvasView */,
165 | F44D53E226AA03FE00A757FB /* ShapeEditApp.swift */,
166 | F44D53E426AA03FE00A757FB /* ContentView.swift */,
167 | F44D53E526AA040000A757FB /* Assets.xcassets */,
168 | );
169 | path = Shared;
170 | sourceTree = "";
171 | };
172 | F44D53EB26AA040000A757FB /* Products */ = {
173 | isa = PBXGroup;
174 | children = (
175 | F44D53EA26AA040000A757FB /* ShapeEdit.app */,
176 | F44D53F226AA040000A757FB /* ShapeEdit.app */,
177 | F44D53FA26AA040000A757FB /* Tests iOS.xctest */,
178 | F44D540526AA040000A757FB /* Tests macOS.xctest */,
179 | );
180 | name = Products;
181 | sourceTree = "";
182 | };
183 | F44D53EC26AA040000A757FB /* iOS */ = {
184 | isa = PBXGroup;
185 | children = (
186 | F44D53ED26AA040000A757FB /* Info.plist */,
187 | );
188 | path = iOS;
189 | sourceTree = "";
190 | };
191 | F44D53F326AA040000A757FB /* macOS */ = {
192 | isa = PBXGroup;
193 | children = (
194 | F44D53F426AA040000A757FB /* Info.plist */,
195 | F44D53F526AA040000A757FB /* macOS.entitlements */,
196 | );
197 | path = macOS;
198 | sourceTree = "";
199 | };
200 | F44D53FD26AA040000A757FB /* Tests iOS */ = {
201 | isa = PBXGroup;
202 | children = (
203 | F44D53FE26AA040000A757FB /* Tests_iOS.swift */,
204 | F44D540026AA040000A757FB /* Info.plist */,
205 | );
206 | path = "Tests iOS";
207 | sourceTree = "";
208 | };
209 | F44D540826AA040000A757FB /* Tests macOS */ = {
210 | isa = PBXGroup;
211 | children = (
212 | F44D540926AA040000A757FB /* Tests_macOS.swift */,
213 | F44D540B26AA040000A757FB /* Info.plist */,
214 | );
215 | path = "Tests macOS";
216 | sourceTree = "";
217 | };
218 | F44D542826AA95F000A757FB /* CanvasView */ = {
219 | isa = PBXGroup;
220 | children = (
221 | F44D542926AA95FE00A757FB /* CanvasView.swift */,
222 | F44D542D26AA987600A757FB /* Utilities.swift */,
223 | F4ED8A1A26B48916004E7307 /* GridView.swift */,
224 | );
225 | path = CanvasView;
226 | sourceTree = "";
227 | };
228 | F44D542C26AA985F00A757FB /* Model */ = {
229 | isa = PBXGroup;
230 | children = (
231 | F44D53E326AA03FE00A757FB /* ShapeEditDocument.swift */,
232 | F44D542226AA0A2E00A757FB /* Graphic.swift */,
233 | F41779E826BFA69000C43EBF /* Graphic+Test.swift */,
234 | F457A1AD26CADCBF003464CA /* Tree.swift */,
235 | );
236 | path = Model;
237 | sourceTree = "";
238 | };
239 | F4ED8A1726B46C02004E7307 /* Frameworks */ = {
240 | isa = PBXGroup;
241 | children = (
242 | );
243 | name = Frameworks;
244 | sourceTree = "";
245 | };
246 | F4ED8A1E26B800C5004E7307 /* Auxiliary */ = {
247 | isa = PBXGroup;
248 | children = (
249 | F4ED8A1F26B80123004E7307 /* NavigatorView.swift */,
250 | F4ED8A2226B8306E004E7307 /* GraphicShapeView.swift */,
251 | F4536CC626B907A400FCD6BA /* SelectionView.swift */,
252 | F47A244E26BAE8D500977136 /* DragInfo.swift */,
253 | F42E979326BC43FC001C37C2 /* LibraryView.swift */,
254 | );
255 | path = Auxiliary;
256 | sourceTree = "";
257 | };
258 | /* End PBXGroup section */
259 |
260 | /* Begin PBXNativeTarget section */
261 | F44D53E926AA040000A757FB /* ShapeEdit (iOS) */ = {
262 | isa = PBXNativeTarget;
263 | buildConfigurationList = F44D541626AA040000A757FB /* Build configuration list for PBXNativeTarget "ShapeEdit (iOS)" */;
264 | buildPhases = (
265 | F44D53E626AA040000A757FB /* Sources */,
266 | F44D53E726AA040000A757FB /* Frameworks */,
267 | F44D53E826AA040000A757FB /* Resources */,
268 | );
269 | buildRules = (
270 | );
271 | dependencies = (
272 | );
273 | name = "ShapeEdit (iOS)";
274 | packageProductDependencies = (
275 | F4ED8A1526B46BEF004E7307 /* AdvancedScrollView */,
276 | );
277 | productName = "ShapeEdit (iOS)";
278 | productReference = F44D53EA26AA040000A757FB /* ShapeEdit.app */;
279 | productType = "com.apple.product-type.application";
280 | };
281 | F44D53F126AA040000A757FB /* ShapeEdit (macOS) */ = {
282 | isa = PBXNativeTarget;
283 | buildConfigurationList = F44D541926AA040000A757FB /* Build configuration list for PBXNativeTarget "ShapeEdit (macOS)" */;
284 | buildPhases = (
285 | F44D53EE26AA040000A757FB /* Sources */,
286 | F44D53EF26AA040000A757FB /* Frameworks */,
287 | F44D53F026AA040000A757FB /* Resources */,
288 | );
289 | buildRules = (
290 | );
291 | dependencies = (
292 | );
293 | name = "ShapeEdit (macOS)";
294 | packageProductDependencies = (
295 | F4ED8A1826B46C02004E7307 /* AdvancedScrollView */,
296 | );
297 | productName = "ShapeEdit (macOS)";
298 | productReference = F44D53F226AA040000A757FB /* ShapeEdit.app */;
299 | productType = "com.apple.product-type.application";
300 | };
301 | F44D53F926AA040000A757FB /* Tests iOS */ = {
302 | isa = PBXNativeTarget;
303 | buildConfigurationList = F44D541C26AA040000A757FB /* Build configuration list for PBXNativeTarget "Tests iOS" */;
304 | buildPhases = (
305 | F44D53F626AA040000A757FB /* Sources */,
306 | F44D53F726AA040000A757FB /* Frameworks */,
307 | F44D53F826AA040000A757FB /* Resources */,
308 | );
309 | buildRules = (
310 | );
311 | dependencies = (
312 | F44D53FC26AA040000A757FB /* PBXTargetDependency */,
313 | );
314 | name = "Tests iOS";
315 | productName = "Tests iOS";
316 | productReference = F44D53FA26AA040000A757FB /* Tests iOS.xctest */;
317 | productType = "com.apple.product-type.bundle.ui-testing";
318 | };
319 | F44D540426AA040000A757FB /* Tests macOS */ = {
320 | isa = PBXNativeTarget;
321 | buildConfigurationList = F44D541F26AA040000A757FB /* Build configuration list for PBXNativeTarget "Tests macOS" */;
322 | buildPhases = (
323 | F44D540126AA040000A757FB /* Sources */,
324 | F44D540226AA040000A757FB /* Frameworks */,
325 | F44D540326AA040000A757FB /* Resources */,
326 | );
327 | buildRules = (
328 | );
329 | dependencies = (
330 | F44D540726AA040000A757FB /* PBXTargetDependency */,
331 | );
332 | name = "Tests macOS";
333 | productName = "Tests macOS";
334 | productReference = F44D540526AA040000A757FB /* Tests macOS.xctest */;
335 | productType = "com.apple.product-type.bundle.ui-testing";
336 | };
337 | /* End PBXNativeTarget section */
338 |
339 | /* Begin PBXProject section */
340 | F44D53DD26AA03FD00A757FB /* Project object */ = {
341 | isa = PBXProject;
342 | attributes = {
343 | LastSwiftUpdateCheck = 1250;
344 | LastUpgradeCheck = 1250;
345 | TargetAttributes = {
346 | F44D53E926AA040000A757FB = {
347 | CreatedOnToolsVersion = 12.5.1;
348 | };
349 | F44D53F126AA040000A757FB = {
350 | CreatedOnToolsVersion = 12.5.1;
351 | };
352 | F44D53F926AA040000A757FB = {
353 | CreatedOnToolsVersion = 12.5.1;
354 | TestTargetID = F44D53E926AA040000A757FB;
355 | };
356 | F44D540426AA040000A757FB = {
357 | CreatedOnToolsVersion = 12.5.1;
358 | TestTargetID = F44D53F126AA040000A757FB;
359 | };
360 | };
361 | };
362 | buildConfigurationList = F44D53E026AA03FD00A757FB /* Build configuration list for PBXProject "ShapeEdit" */;
363 | compatibilityVersion = "Xcode 9.3";
364 | developmentRegion = en;
365 | hasScannedForEncodings = 0;
366 | knownRegions = (
367 | en,
368 | Base,
369 | );
370 | mainGroup = F44D53DC26AA03FD00A757FB;
371 | packageReferences = (
372 | F4ED8A1426B46BEF004E7307 /* XCRemoteSwiftPackageReference "advanced-scrollview" */,
373 | );
374 | productRefGroup = F44D53EB26AA040000A757FB /* Products */;
375 | projectDirPath = "";
376 | projectRoot = "";
377 | targets = (
378 | F44D53E926AA040000A757FB /* ShapeEdit (iOS) */,
379 | F44D53F126AA040000A757FB /* ShapeEdit (macOS) */,
380 | F44D53F926AA040000A757FB /* Tests iOS */,
381 | F44D540426AA040000A757FB /* Tests macOS */,
382 | );
383 | };
384 | /* End PBXProject section */
385 |
386 | /* Begin PBXResourcesBuildPhase section */
387 | F44D53E826AA040000A757FB /* Resources */ = {
388 | isa = PBXResourcesBuildPhase;
389 | buildActionMask = 2147483647;
390 | files = (
391 | F44D541226AA040000A757FB /* Assets.xcassets in Resources */,
392 | );
393 | runOnlyForDeploymentPostprocessing = 0;
394 | };
395 | F44D53F026AA040000A757FB /* Resources */ = {
396 | isa = PBXResourcesBuildPhase;
397 | buildActionMask = 2147483647;
398 | files = (
399 | F44D541326AA040000A757FB /* Assets.xcassets in Resources */,
400 | );
401 | runOnlyForDeploymentPostprocessing = 0;
402 | };
403 | F44D53F826AA040000A757FB /* Resources */ = {
404 | isa = PBXResourcesBuildPhase;
405 | buildActionMask = 2147483647;
406 | files = (
407 | );
408 | runOnlyForDeploymentPostprocessing = 0;
409 | };
410 | F44D540326AA040000A757FB /* Resources */ = {
411 | isa = PBXResourcesBuildPhase;
412 | buildActionMask = 2147483647;
413 | files = (
414 | );
415 | runOnlyForDeploymentPostprocessing = 0;
416 | };
417 | /* End PBXResourcesBuildPhase section */
418 |
419 | /* Begin PBXSourcesBuildPhase section */
420 | F44D53E626AA040000A757FB /* Sources */ = {
421 | isa = PBXSourcesBuildPhase;
422 | buildActionMask = 2147483647;
423 | files = (
424 | F457A1AE26CADCBF003464CA /* Tree.swift in Sources */,
425 | F44D542E26AA987600A757FB /* Utilities.swift in Sources */,
426 | F44D540E26AA040000A757FB /* ShapeEditDocument.swift in Sources */,
427 | F4ED8A1B26B48916004E7307 /* GridView.swift in Sources */,
428 | F47A244F26BAE8D500977136 /* DragInfo.swift in Sources */,
429 | F42E979426BC43FC001C37C2 /* LibraryView.swift in Sources */,
430 | F4ED8A2326B8306E004E7307 /* GraphicShapeView.swift in Sources */,
431 | F44D540C26AA040000A757FB /* ShapeEditApp.swift in Sources */,
432 | F44A417426CD864E007EF5B6 /* LineWidthRowView.swift in Sources */,
433 | F4ED8A2026B80123004E7307 /* NavigatorView.swift in Sources */,
434 | F44D542A26AA95FE00A757FB /* CanvasView.swift in Sources */,
435 | F44D542326AA0A2F00A757FB /* Graphic.swift in Sources */,
436 | F41779E926BFA69000C43EBF /* Graphic+Test.swift in Sources */,
437 | F428FBB626BD99A300C07179 /* InspectorView.swift in Sources */,
438 | F44D541026AA040000A757FB /* ContentView.swift in Sources */,
439 | F4536CC726B907A400FCD6BA /* SelectionView.swift in Sources */,
440 | F44A417126CD8365007EF5B6 /* ColorRowView.swift in Sources */,
441 | );
442 | runOnlyForDeploymentPostprocessing = 0;
443 | };
444 | F44D53EE26AA040000A757FB /* Sources */ = {
445 | isa = PBXSourcesBuildPhase;
446 | buildActionMask = 2147483647;
447 | files = (
448 | F457A1AF26CADCC0003464CA /* Tree.swift in Sources */,
449 | F44D542F26AA987600A757FB /* Utilities.swift in Sources */,
450 | F44D540F26AA040000A757FB /* ShapeEditDocument.swift in Sources */,
451 | F4ED8A1C26B48916004E7307 /* GridView.swift in Sources */,
452 | F47A245026BAE8D500977136 /* DragInfo.swift in Sources */,
453 | F42E979526BC43FC001C37C2 /* LibraryView.swift in Sources */,
454 | F4ED8A2426B8306E004E7307 /* GraphicShapeView.swift in Sources */,
455 | F44D540D26AA040000A757FB /* ShapeEditApp.swift in Sources */,
456 | F44A417526CD864E007EF5B6 /* LineWidthRowView.swift in Sources */,
457 | F4ED8A2126B80123004E7307 /* NavigatorView.swift in Sources */,
458 | F44D542B26AA95FE00A757FB /* CanvasView.swift in Sources */,
459 | F44D542426AA0A2F00A757FB /* Graphic.swift in Sources */,
460 | F41779EA26BFA69000C43EBF /* Graphic+Test.swift in Sources */,
461 | F428FBB726BD99A400C07179 /* InspectorView.swift in Sources */,
462 | F44D541126AA040000A757FB /* ContentView.swift in Sources */,
463 | F4536CC826B907A400FCD6BA /* SelectionView.swift in Sources */,
464 | F44A417226CD8365007EF5B6 /* ColorRowView.swift in Sources */,
465 | );
466 | runOnlyForDeploymentPostprocessing = 0;
467 | };
468 | F44D53F626AA040000A757FB /* Sources */ = {
469 | isa = PBXSourcesBuildPhase;
470 | buildActionMask = 2147483647;
471 | files = (
472 | F44D53FF26AA040000A757FB /* Tests_iOS.swift in Sources */,
473 | );
474 | runOnlyForDeploymentPostprocessing = 0;
475 | };
476 | F44D540126AA040000A757FB /* Sources */ = {
477 | isa = PBXSourcesBuildPhase;
478 | buildActionMask = 2147483647;
479 | files = (
480 | F44D540A26AA040000A757FB /* Tests_macOS.swift in Sources */,
481 | );
482 | runOnlyForDeploymentPostprocessing = 0;
483 | };
484 | /* End PBXSourcesBuildPhase section */
485 |
486 | /* Begin PBXTargetDependency section */
487 | F44D53FC26AA040000A757FB /* PBXTargetDependency */ = {
488 | isa = PBXTargetDependency;
489 | target = F44D53E926AA040000A757FB /* ShapeEdit (iOS) */;
490 | targetProxy = F44D53FB26AA040000A757FB /* PBXContainerItemProxy */;
491 | };
492 | F44D540726AA040000A757FB /* PBXTargetDependency */ = {
493 | isa = PBXTargetDependency;
494 | target = F44D53F126AA040000A757FB /* ShapeEdit (macOS) */;
495 | targetProxy = F44D540626AA040000A757FB /* PBXContainerItemProxy */;
496 | };
497 | /* End PBXTargetDependency section */
498 |
499 | /* Begin XCBuildConfiguration section */
500 | F44D541426AA040000A757FB /* Debug */ = {
501 | isa = XCBuildConfiguration;
502 | buildSettings = {
503 | ALWAYS_SEARCH_USER_PATHS = NO;
504 | CLANG_ANALYZER_NONNULL = YES;
505 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
506 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
507 | CLANG_CXX_LIBRARY = "libc++";
508 | CLANG_ENABLE_MODULES = YES;
509 | CLANG_ENABLE_OBJC_ARC = YES;
510 | CLANG_ENABLE_OBJC_WEAK = YES;
511 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
512 | CLANG_WARN_BOOL_CONVERSION = YES;
513 | CLANG_WARN_COMMA = YES;
514 | CLANG_WARN_CONSTANT_CONVERSION = YES;
515 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
516 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
517 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
518 | CLANG_WARN_EMPTY_BODY = YES;
519 | CLANG_WARN_ENUM_CONVERSION = YES;
520 | CLANG_WARN_INFINITE_RECURSION = YES;
521 | CLANG_WARN_INT_CONVERSION = YES;
522 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
523 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
524 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
525 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
526 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
527 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
528 | CLANG_WARN_STRICT_PROTOTYPES = YES;
529 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
530 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
531 | CLANG_WARN_UNREACHABLE_CODE = YES;
532 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
533 | COPY_PHASE_STRIP = NO;
534 | DEBUG_INFORMATION_FORMAT = dwarf;
535 | ENABLE_STRICT_OBJC_MSGSEND = YES;
536 | ENABLE_TESTABILITY = YES;
537 | GCC_C_LANGUAGE_STANDARD = gnu11;
538 | GCC_DYNAMIC_NO_PIC = NO;
539 | GCC_NO_COMMON_BLOCKS = YES;
540 | GCC_OPTIMIZATION_LEVEL = 0;
541 | GCC_PREPROCESSOR_DEFINITIONS = (
542 | "DEBUG=1",
543 | "$(inherited)",
544 | );
545 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
546 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
547 | GCC_WARN_UNDECLARED_SELECTOR = YES;
548 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
549 | GCC_WARN_UNUSED_FUNCTION = YES;
550 | GCC_WARN_UNUSED_VARIABLE = YES;
551 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
552 | MTL_FAST_MATH = YES;
553 | ONLY_ACTIVE_ARCH = YES;
554 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
555 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
556 | };
557 | name = Debug;
558 | };
559 | F44D541526AA040000A757FB /* Release */ = {
560 | isa = XCBuildConfiguration;
561 | buildSettings = {
562 | ALWAYS_SEARCH_USER_PATHS = NO;
563 | CLANG_ANALYZER_NONNULL = YES;
564 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
565 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
566 | CLANG_CXX_LIBRARY = "libc++";
567 | CLANG_ENABLE_MODULES = YES;
568 | CLANG_ENABLE_OBJC_ARC = YES;
569 | CLANG_ENABLE_OBJC_WEAK = YES;
570 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
571 | CLANG_WARN_BOOL_CONVERSION = YES;
572 | CLANG_WARN_COMMA = YES;
573 | CLANG_WARN_CONSTANT_CONVERSION = YES;
574 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
575 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
576 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
577 | CLANG_WARN_EMPTY_BODY = YES;
578 | CLANG_WARN_ENUM_CONVERSION = YES;
579 | CLANG_WARN_INFINITE_RECURSION = YES;
580 | CLANG_WARN_INT_CONVERSION = YES;
581 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
582 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
583 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
584 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
585 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
586 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
587 | CLANG_WARN_STRICT_PROTOTYPES = YES;
588 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
589 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
590 | CLANG_WARN_UNREACHABLE_CODE = YES;
591 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
592 | COPY_PHASE_STRIP = NO;
593 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
594 | ENABLE_NS_ASSERTIONS = NO;
595 | ENABLE_STRICT_OBJC_MSGSEND = YES;
596 | GCC_C_LANGUAGE_STANDARD = gnu11;
597 | GCC_NO_COMMON_BLOCKS = YES;
598 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
599 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
600 | GCC_WARN_UNDECLARED_SELECTOR = YES;
601 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
602 | GCC_WARN_UNUSED_FUNCTION = YES;
603 | GCC_WARN_UNUSED_VARIABLE = YES;
604 | MTL_ENABLE_DEBUG_INFO = NO;
605 | MTL_FAST_MATH = YES;
606 | SWIFT_COMPILATION_MODE = wholemodule;
607 | SWIFT_OPTIMIZATION_LEVEL = "-O";
608 | };
609 | name = Release;
610 | };
611 | F44D541726AA040000A757FB /* Debug */ = {
612 | isa = XCBuildConfiguration;
613 | buildSettings = {
614 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
615 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
616 | CODE_SIGN_STYLE = Automatic;
617 | DEVELOPMENT_TEAM = UEQ8YHF529;
618 | ENABLE_PREVIEWS = YES;
619 | INFOPLIST_FILE = iOS/Info.plist;
620 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
621 | LD_RUNPATH_SEARCH_PATHS = (
622 | "$(inherited)",
623 | "@executable_path/Frameworks",
624 | );
625 | PRODUCT_BUNDLE_IDENTIFIER = org.danokhin.ShapeEdit;
626 | PRODUCT_NAME = ShapeEdit;
627 | SDKROOT = iphoneos;
628 | SWIFT_VERSION = 5.0;
629 | TARGETED_DEVICE_FAMILY = "1,2";
630 | };
631 | name = Debug;
632 | };
633 | F44D541826AA040000A757FB /* Release */ = {
634 | isa = XCBuildConfiguration;
635 | buildSettings = {
636 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
637 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
638 | CODE_SIGN_STYLE = Automatic;
639 | DEVELOPMENT_TEAM = UEQ8YHF529;
640 | ENABLE_PREVIEWS = YES;
641 | INFOPLIST_FILE = iOS/Info.plist;
642 | IPHONEOS_DEPLOYMENT_TARGET = 14.0;
643 | LD_RUNPATH_SEARCH_PATHS = (
644 | "$(inherited)",
645 | "@executable_path/Frameworks",
646 | );
647 | PRODUCT_BUNDLE_IDENTIFIER = org.danokhin.ShapeEdit;
648 | PRODUCT_NAME = ShapeEdit;
649 | SDKROOT = iphoneos;
650 | SWIFT_VERSION = 5.0;
651 | TARGETED_DEVICE_FAMILY = "1,2";
652 | VALIDATE_PRODUCT = YES;
653 | };
654 | name = Release;
655 | };
656 | F44D541A26AA040000A757FB /* Debug */ = {
657 | isa = XCBuildConfiguration;
658 | buildSettings = {
659 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
660 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
661 | CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
662 | CODE_SIGN_STYLE = Automatic;
663 | COMBINE_HIDPI_IMAGES = YES;
664 | DEVELOPMENT_TEAM = UEQ8YHF529;
665 | ENABLE_HARDENED_RUNTIME = YES;
666 | ENABLE_PREVIEWS = YES;
667 | INFOPLIST_FILE = macOS/Info.plist;
668 | LD_RUNPATH_SEARCH_PATHS = (
669 | "$(inherited)",
670 | "@executable_path/../Frameworks",
671 | );
672 | MACOSX_DEPLOYMENT_TARGET = 11.0;
673 | PRODUCT_BUNDLE_IDENTIFIER = org.danokhin.ShapeEdit;
674 | PRODUCT_NAME = ShapeEdit;
675 | SDKROOT = macosx;
676 | SWIFT_VERSION = 5.0;
677 | };
678 | name = Debug;
679 | };
680 | F44D541B26AA040000A757FB /* Release */ = {
681 | isa = XCBuildConfiguration;
682 | buildSettings = {
683 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
684 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
685 | CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
686 | CODE_SIGN_STYLE = Automatic;
687 | COMBINE_HIDPI_IMAGES = YES;
688 | DEVELOPMENT_TEAM = UEQ8YHF529;
689 | ENABLE_HARDENED_RUNTIME = YES;
690 | ENABLE_PREVIEWS = YES;
691 | INFOPLIST_FILE = macOS/Info.plist;
692 | LD_RUNPATH_SEARCH_PATHS = (
693 | "$(inherited)",
694 | "@executable_path/../Frameworks",
695 | );
696 | MACOSX_DEPLOYMENT_TARGET = 11.0;
697 | PRODUCT_BUNDLE_IDENTIFIER = org.danokhin.ShapeEdit;
698 | PRODUCT_NAME = ShapeEdit;
699 | SDKROOT = macosx;
700 | SWIFT_VERSION = 5.0;
701 | };
702 | name = Release;
703 | };
704 | F44D541D26AA040000A757FB /* Debug */ = {
705 | isa = XCBuildConfiguration;
706 | buildSettings = {
707 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
708 | CODE_SIGN_STYLE = Automatic;
709 | DEVELOPMENT_TEAM = UEQ8YHF529;
710 | INFOPLIST_FILE = "Tests iOS/Info.plist";
711 | IPHONEOS_DEPLOYMENT_TARGET = 14.5;
712 | LD_RUNPATH_SEARCH_PATHS = (
713 | "$(inherited)",
714 | "@executable_path/Frameworks",
715 | "@loader_path/Frameworks",
716 | );
717 | PRODUCT_BUNDLE_IDENTIFIER = "org.danokhin.Tests-iOS";
718 | PRODUCT_NAME = "$(TARGET_NAME)";
719 | SDKROOT = iphoneos;
720 | SWIFT_VERSION = 5.0;
721 | TARGETED_DEVICE_FAMILY = "1,2";
722 | TEST_TARGET_NAME = "ShapeEdit (iOS)";
723 | };
724 | name = Debug;
725 | };
726 | F44D541E26AA040000A757FB /* Release */ = {
727 | isa = XCBuildConfiguration;
728 | buildSettings = {
729 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
730 | CODE_SIGN_STYLE = Automatic;
731 | DEVELOPMENT_TEAM = UEQ8YHF529;
732 | INFOPLIST_FILE = "Tests iOS/Info.plist";
733 | IPHONEOS_DEPLOYMENT_TARGET = 14.5;
734 | LD_RUNPATH_SEARCH_PATHS = (
735 | "$(inherited)",
736 | "@executable_path/Frameworks",
737 | "@loader_path/Frameworks",
738 | );
739 | PRODUCT_BUNDLE_IDENTIFIER = "org.danokhin.Tests-iOS";
740 | PRODUCT_NAME = "$(TARGET_NAME)";
741 | SDKROOT = iphoneos;
742 | SWIFT_VERSION = 5.0;
743 | TARGETED_DEVICE_FAMILY = "1,2";
744 | TEST_TARGET_NAME = "ShapeEdit (iOS)";
745 | VALIDATE_PRODUCT = YES;
746 | };
747 | name = Release;
748 | };
749 | F44D542026AA040000A757FB /* Debug */ = {
750 | isa = XCBuildConfiguration;
751 | buildSettings = {
752 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
753 | CODE_SIGN_STYLE = Automatic;
754 | COMBINE_HIDPI_IMAGES = YES;
755 | DEVELOPMENT_TEAM = UEQ8YHF529;
756 | INFOPLIST_FILE = "Tests macOS/Info.plist";
757 | LD_RUNPATH_SEARCH_PATHS = (
758 | "$(inherited)",
759 | "@executable_path/../Frameworks",
760 | "@loader_path/../Frameworks",
761 | );
762 | MACOSX_DEPLOYMENT_TARGET = 11.2;
763 | PRODUCT_BUNDLE_IDENTIFIER = "org.danokhin.Tests-macOS";
764 | PRODUCT_NAME = "$(TARGET_NAME)";
765 | SDKROOT = macosx;
766 | SWIFT_VERSION = 5.0;
767 | TEST_TARGET_NAME = "ShapeEdit (macOS)";
768 | };
769 | name = Debug;
770 | };
771 | F44D542126AA040000A757FB /* Release */ = {
772 | isa = XCBuildConfiguration;
773 | buildSettings = {
774 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
775 | CODE_SIGN_STYLE = Automatic;
776 | COMBINE_HIDPI_IMAGES = YES;
777 | DEVELOPMENT_TEAM = UEQ8YHF529;
778 | INFOPLIST_FILE = "Tests macOS/Info.plist";
779 | LD_RUNPATH_SEARCH_PATHS = (
780 | "$(inherited)",
781 | "@executable_path/../Frameworks",
782 | "@loader_path/../Frameworks",
783 | );
784 | MACOSX_DEPLOYMENT_TARGET = 11.2;
785 | PRODUCT_BUNDLE_IDENTIFIER = "org.danokhin.Tests-macOS";
786 | PRODUCT_NAME = "$(TARGET_NAME)";
787 | SDKROOT = macosx;
788 | SWIFT_VERSION = 5.0;
789 | TEST_TARGET_NAME = "ShapeEdit (macOS)";
790 | };
791 | name = Release;
792 | };
793 | /* End XCBuildConfiguration section */
794 |
795 | /* Begin XCConfigurationList section */
796 | F44D53E026AA03FD00A757FB /* Build configuration list for PBXProject "ShapeEdit" */ = {
797 | isa = XCConfigurationList;
798 | buildConfigurations = (
799 | F44D541426AA040000A757FB /* Debug */,
800 | F44D541526AA040000A757FB /* Release */,
801 | );
802 | defaultConfigurationIsVisible = 0;
803 | defaultConfigurationName = Release;
804 | };
805 | F44D541626AA040000A757FB /* Build configuration list for PBXNativeTarget "ShapeEdit (iOS)" */ = {
806 | isa = XCConfigurationList;
807 | buildConfigurations = (
808 | F44D541726AA040000A757FB /* Debug */,
809 | F44D541826AA040000A757FB /* Release */,
810 | );
811 | defaultConfigurationIsVisible = 0;
812 | defaultConfigurationName = Release;
813 | };
814 | F44D541926AA040000A757FB /* Build configuration list for PBXNativeTarget "ShapeEdit (macOS)" */ = {
815 | isa = XCConfigurationList;
816 | buildConfigurations = (
817 | F44D541A26AA040000A757FB /* Debug */,
818 | F44D541B26AA040000A757FB /* Release */,
819 | );
820 | defaultConfigurationIsVisible = 0;
821 | defaultConfigurationName = Release;
822 | };
823 | F44D541C26AA040000A757FB /* Build configuration list for PBXNativeTarget "Tests iOS" */ = {
824 | isa = XCConfigurationList;
825 | buildConfigurations = (
826 | F44D541D26AA040000A757FB /* Debug */,
827 | F44D541E26AA040000A757FB /* Release */,
828 | );
829 | defaultConfigurationIsVisible = 0;
830 | defaultConfigurationName = Release;
831 | };
832 | F44D541F26AA040000A757FB /* Build configuration list for PBXNativeTarget "Tests macOS" */ = {
833 | isa = XCConfigurationList;
834 | buildConfigurations = (
835 | F44D542026AA040000A757FB /* Debug */,
836 | F44D542126AA040000A757FB /* Release */,
837 | );
838 | defaultConfigurationIsVisible = 0;
839 | defaultConfigurationName = Release;
840 | };
841 | /* End XCConfigurationList section */
842 |
843 | /* Begin XCRemoteSwiftPackageReference section */
844 | F4ED8A1426B46BEF004E7307 /* XCRemoteSwiftPackageReference "advanced-scrollview" */ = {
845 | isa = XCRemoteSwiftPackageReference;
846 | repositoryURL = "https://github.com/dmytro-anokhin/advanced-scrollview";
847 | requirement = {
848 | kind = upToNextMajorVersion;
849 | minimumVersion = 0.0.5;
850 | };
851 | };
852 | /* End XCRemoteSwiftPackageReference section */
853 |
854 | /* Begin XCSwiftPackageProductDependency section */
855 | F4ED8A1526B46BEF004E7307 /* AdvancedScrollView */ = {
856 | isa = XCSwiftPackageProductDependency;
857 | package = F4ED8A1426B46BEF004E7307 /* XCRemoteSwiftPackageReference "advanced-scrollview" */;
858 | productName = AdvancedScrollView;
859 | };
860 | F4ED8A1826B46C02004E7307 /* AdvancedScrollView */ = {
861 | isa = XCSwiftPackageProductDependency;
862 | package = F4ED8A1426B46BEF004E7307 /* XCRemoteSwiftPackageReference "advanced-scrollview" */;
863 | productName = AdvancedScrollView;
864 | };
865 | /* End XCSwiftPackageProductDependency section */
866 | };
867 | rootObject = F44D53DD26AA03FD00A757FB /* Project object */;
868 | }
869 |
--------------------------------------------------------------------------------