├── Images
├── color-picker.png
└── susketch view.png
├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── xcshareddata
│ └── IDEWorkspaceChecks.plist
├── Tests
└── SUSketchTests
│ └── SUSketchTests.swift
├── Package.swift
├── LICENSE.txt
├── Sources
└── SUSketch
│ ├── DrawingTool.swift
│ ├── ImagePicker.swift
│ ├── DrawingData.swift
│ └── SUSketchView.swift
└── README.md
/Images/color-picker.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArunaUd92/SUSketch/HEAD/Images/color-picker.png
--------------------------------------------------------------------------------
/Images/susketch view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ArunaUd92/SUSketch/HEAD/Images/susketch view.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /.build
3 | /Packages
4 | xcuserdata/
5 | DerivedData/
6 | .swiftpm/configuration/registries.json
7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
8 | .netrc
9 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Tests/SUSketchTests/SUSketchTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import SUSketch
3 |
4 | final class SUSketchTests: XCTestCase {
5 | func testExample() throws {
6 | // XCTest Documentation
7 | // https://developer.apple.com/documentation/xctest
8 |
9 | // Defining Test Cases and Test Methods
10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "SUSketch",
8 | platforms: [
9 | .iOS(.v15),
10 | ],
11 | products: [
12 | // Products define the executables and libraries a package produces, making them visible to other packages.
13 | .library(
14 | name: "SUSketch",
15 | targets: ["SUSketch"]),
16 | ],
17 | targets: [
18 | // Targets are the basic building blocks of a package, defining a module or a test suite.
19 | // Targets can depend on other targets in this package and products from dependencies.
20 | .target(
21 | name: "SUSketch"),
22 | .testTarget(
23 | name: "SUSketchTests",
24 | dependencies: ["SUSketch"]),
25 | ]
26 | )
27 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2024 Aruna Udayanga
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/Sources/SUSketch/DrawingTool.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Aruna Udayanga on 23/04/2024.
6 | //
7 |
8 | import SwiftUI
9 |
10 | enum DrawingTool: String, CaseIterable, Identifiable {
11 | case pen
12 | case eraser
13 | case brush
14 | case fill
15 |
16 | var id: String { self.rawValue }
17 |
18 | var description: String {
19 | switch self {
20 | case .pen: return "Pen"
21 | case .eraser: return "Eraser"
22 | case .brush: return "Brush"
23 | case .fill: return "Fill"
24 | }
25 | }
26 |
27 | var iconName: String {
28 | switch self {
29 | case .pen: return "pencil"
30 | case .eraser: return "eraser"
31 | case .brush: return "paintbrush"
32 | case .fill: return "eyedropper"
33 | }
34 | }
35 | }
36 |
37 | struct ShapeElement: Identifiable {
38 | let id: UUID = UUID()
39 | var type: ShapeType
40 | var position: CGPoint
41 | var size: CGSize
42 | var color: Color
43 | var isSelected: Bool = false
44 |
45 | enum ShapeType {
46 | case triangle, rectangle, circle, oval, square
47 |
48 | var description: String {
49 | switch self {
50 | case .triangle: return "Triangle"
51 | case .rectangle: return "Rectangle"
52 | case .circle: return "Circle"
53 | case .oval: return "Oval"
54 | case .square: return "Square"
55 | }
56 | }
57 | }
58 | }
59 |
60 |
61 |
--------------------------------------------------------------------------------
/Sources/SUSketch/ImagePicker.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImagePicker.swift
3 | // SUSketch
4 | //
5 | // Created by Aruna Udayanga on 14/05/2024.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 | import PhotosUI
11 |
12 | struct ImagePicker: UIViewControllerRepresentable {
13 | @Environment(\.presentationMode) var presentationMode
14 | @Binding var image: UIImage?
15 |
16 | func makeUIViewController(context: UIViewControllerRepresentableContext) -> UIImagePickerController {
17 | let picker = UIImagePickerController()
18 | picker.delegate = context.coordinator
19 | return picker
20 | }
21 |
22 | func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext) {
23 | // No update action needed
24 | }
25 |
26 | func makeCoordinator() -> Coordinator {
27 | Coordinator(self)
28 | }
29 |
30 | class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
31 | let parent: ImagePicker
32 |
33 | init(_ parent: ImagePicker) {
34 | self.parent = parent
35 | }
36 |
37 | func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
38 | if let uiImage = info[.originalImage] as? UIImage {
39 | parent.image = uiImage
40 | }
41 | parent.presentationMode.wrappedValue.dismiss()
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SUSketch
2 |
3 | 
4 | [](https://github.com/ArunaUd92/SUSketch)
5 | [](https://developer.apple.com/swift)
6 | [](https://www.swift.org/documentation/package-manager/)
7 |
8 | SUSketch has basic functions for drawing from the beginning. Anyone can easily create drawing iOS Applications.
9 |
10 | 
11 | 
12 |
13 | ## Features
14 | - [x] Pen tool
15 | - [x] Eraser tool
16 | - [x] Fill
17 | - [x] Brush
18 | - [x] Undo / Redo
19 | - [x] Draw on Camera / Gallery image
20 | - [x] Text
21 |
22 | ## Requirements
23 | - Xcode 14
24 | - Swift 5.5+
25 |
26 | ## Installation
27 |
28 | Swift Package Manager
29 |
30 | The [Swift Package Manager](https://www.swift.org/package-manager/) is a tool for automating the distribution of Swift code and is integrated into the swift compiler.
31 |
32 | Once you have your Swift package set up, adding SUSketch as a dependency is as easy as adding it to the dependencies value of your Package.swift.
33 |
34 | ```swift
35 | dependencies: [
36 | .package(url: "https://github.com/ArunaUd92/SUSketch")
37 | ]
38 | ```
39 |
40 | ## Privacy
41 |
42 | `SUSketch` does not collect any data. A [privacy manifest file](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files).
43 |
44 | ## License
45 |
46 | SUSketch is available under the MIT license. See the LICENSE file for more info.
47 |
--------------------------------------------------------------------------------
/Sources/SUSketch/DrawingData.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DrawingData.swift
3 | // SUSketch
4 | //
5 | // Created by Aruna Udayanga on 23/04/2024.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct TextElement: Identifiable, Equatable {
11 | let id: UUID = UUID()
12 | var text: String
13 | var position: CGPoint
14 | var color: Color
15 | var fontSize: CGFloat
16 | var isSelected: Bool = false
17 | }
18 |
19 | // Observable class to manage and update drawing data.
20 | public class DrawingData: ObservableObject {
21 | @Published var paths: [(path: Path, color: Color, isFilled: Bool, lineWidth: CGFloat)] = [] // Stores drawn paths.
22 | @Published var currentTool: DrawingTool = .pen // Active drawing tool.
23 | @Published var penColor: Color = .black // Default color for pen.
24 | @Published var penWidth: CGFloat = 1.0 // Default width for pen.
25 | @Published var brushWidth: CGFloat = 10.0 // Default width for brush.
26 | @Published var image: Image? = nil // Optional image that can be added to the canvas.
27 | @Published var texts: [TextElement] = [] // Array of text elements.
28 | @Published var shapes: [ShapeElement] = [] // Array of shape elements.
29 | @Published var currentShape: ShapeElement.ShapeType? // Currently selected shape
30 |
31 |
32 | // Computed property to adjust the line width based on the current tool.
33 | var currentWidth: CGFloat {
34 | get {
35 | switch currentTool {
36 | case .pen:
37 | return penWidth
38 | case .brush:
39 | return brushWidth
40 | default:
41 | return 1.0 // Default width for other tools.
42 | }
43 | }
44 | set {
45 | switch currentTool {
46 | case .pen:
47 | penWidth = newValue
48 | case .brush:
49 | brushWidth = newValue
50 | default:
51 | break
52 | }
53 | }
54 | }
55 |
56 | // Array to store paths for undo functionality.
57 | var history: [(path: Path, color: Color, isFilled: Bool, lineWidth: CGFloat)] = []
58 |
59 | // Adds a path to the paths array.
60 | func addPath(_ path: Path, color: Color, isFilled: Bool = false, lineWidth: CGFloat = 5.0) {
61 | paths.append((path, color, isFilled, lineWidth))
62 | }
63 |
64 | // Undo the last drawing action.
65 | func undo() {
66 | if let last = paths.popLast() {
67 | history.append(last)
68 | }
69 | }
70 |
71 | // Redo the last undone drawing action.
72 | func redo() {
73 | if let redoPath = history.popLast() {
74 | paths.append(redoPath)
75 | }
76 | }
77 |
78 | // Clears all drawing data.
79 | func clear() {
80 | paths.removeAll()
81 | history.removeAll()
82 | shapes.removeAll()
83 | texts.removeAll()
84 | }
85 |
86 | // Adds a shape to the shapes array.
87 | func addShape(_ shapeType: ShapeElement.ShapeType) {
88 | let shape = ShapeElement(type: shapeType, position: CGPoint(x: 100, y: 100), size: CGSize(width: 100, height: 140), color: penColor)
89 | shapes.append(shape)
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Sources/SUSketch/SUSketchView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Aruna Udayanga on 23/04/2024.
6 | //
7 |
8 | import SwiftUI
9 | import PhotosUI
10 |
11 | // SwiftUI view for a sketching app.
12 | public struct SUSketchView: View {
13 | @StateObject private var drawingData = DrawingData() // Manages drawing data.
14 | @State private var currentPath = Path() // Current drawing path.
15 | @State private var lastLocation: CGPoint? // Last touch point.
16 | @State private var showImagePicker = false // Toggle for image picker.
17 | @State private var inputImage: UIImage? // Selected image.
18 | @State private var inputText: String = "" // User-entered text.
19 | @State private var showTextEditor: Bool = false // Toggle for text editor.
20 | @State private var textColor: Color = .black // Color for text.
21 | @State private var fontSize: CGFloat = 20 // Font size for text.
22 | @State private var cursorLocation: CGPoint = .zero
23 |
24 | public init() {} // Empty initializer for public struct.
25 |
26 | public var body: some View {
27 | GeometryReader { geometry in
28 | VStack {
29 | // Header for tools like text editor and image picker.
30 | SketchHeaderView(
31 | showTextEditor: $showTextEditor,
32 | inputText: $inputText,
33 | textColor: $textColor,
34 | fontSize: $fontSize,
35 | showImagePicker: $showImagePicker,
36 | inputImage: $inputImage,
37 | drawingData: drawingData
38 | )
39 | // Main drawing area handling touch and drawing.
40 | SketchBodyView(
41 | drawingData: drawingData,
42 | currentPath: $currentPath,
43 | lastLocation: $lastLocation,
44 | cursorLocation: $cursorLocation
45 | )
46 | // Footer for additional controls or status.
47 | SketchFooterView(drawingData: drawingData)
48 | }
49 | }
50 | }
51 | }
52 |
53 | struct ContentView_Previews: PreviewProvider {
54 | static var previews: some View {
55 | SUSketchView()
56 | }
57 | }
58 |
59 | struct SketchBodyView: View {
60 | @ObservedObject var drawingData: DrawingData // Observes drawing data for changes.
61 | @Binding var currentPath: Path // Binding to the current drawing path.
62 | @Binding var lastLocation: CGPoint? // Binding to track last touch location.
63 | @State private var inputImage: UIImage? // Holds the image selected from the picker.
64 | @Binding var cursorLocation: CGPoint // Binding to track cursor location.
65 |
66 |
67 | var body: some View {
68 | ZStack {
69 | Canvas { context, size in
70 | // Draw the input image if available.
71 | if let uiImage = inputImage {
72 | let image = Image(uiImage: uiImage)
73 | image.resizable().aspectRatio(contentMode: .fit).frame(width: size.width, height: size.height)
74 | }
75 | // Render background or foreground images and fills.
76 | if let image = drawingData.image {
77 | context.draw(image, in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
78 | }
79 | if drawingData.currentTool == .fill {
80 | context.fill(Rectangle().path(in: CGRect(x: 0, y: 0, width: size.width, height: size.height)), with: .color(drawingData.penColor))
81 | }
82 | // Render each path with appropriate style.
83 | for (path, color, isFilled, lineWidth) in drawingData.paths {
84 | if isFilled {
85 | context.fill(path, with: .color(color))
86 | } else {
87 | context.stroke(path, with: .color(color), lineWidth: lineWidth)
88 | }
89 | }
90 |
91 | // // Render each shape with appropriate style.
92 | // for shape in drawingData.shapes {
93 | // let rect = CGRect(origin: shape.position, size: shape.size)
94 | // switch shape.type {
95 | // case .triangle:
96 | // let path = Path { path in
97 | // path.move(to: CGPoint(x: rect.midX, y: rect.minY))
98 | // path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
99 | // path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
100 | // path.closeSubpath()
101 | // }
102 | // context.stroke(path, with: .color(shape.color))
103 | // case .rectangle:
104 | // context.stroke(Rectangle().path(in: rect), with: .color(shape.color))
105 | // case .circle:
106 | // context.stroke(Circle().path(in: rect), with: .color(shape.color))
107 | // case .oval:
108 | // context.stroke(Ellipse().path(in: rect), with: .color(shape.color))
109 | // case .square:
110 | // let side = min(rect.width, rect.height)
111 | // let squareRect = CGRect(x: rect.origin.x, y: rect.origin.y, width: side, height: side)
112 | // context.stroke(Rectangle().path(in: squareRect), with: .color(shape.color))
113 | // }
114 | //
115 | // }
116 | }
117 |
118 | // Render draggable and resizable shape elements.
119 | ForEach(drawingData.shapes.indices, id: \.self) { index in
120 | let shape = drawingData.shapes[index]
121 | GeometryReader { geometry in
122 | let rect = CGRect(origin: shape.position, size: shape.size)
123 | let handleSize: CGFloat = 20
124 | let handleOffset: CGSize = CGSize(width: shape.size.width + handleSize / 2, height: shape.size.height + handleSize / 2)
125 |
126 | ZStack {
127 | // Shape view
128 | Path { path in
129 | switch shape.type {
130 | case .triangle:
131 | path.move(to: CGPoint(x: rect.midX, y: rect.minY))
132 | path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
133 | path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
134 | path.closeSubpath()
135 | case .rectangle:
136 | path.addRect(rect)
137 | case .circle:
138 | path.addEllipse(in: rect)
139 | case .oval:
140 | path.addEllipse(in: rect)
141 | case .square:
142 | let side = min(rect.width, rect.height)
143 | let squareRect = CGRect(x: rect.origin.x, y: rect.origin.y, width: side, height: side)
144 | path.addRect(squareRect)
145 | }
146 | }
147 | .stroke(shape.color)
148 | .position(shape.position)
149 | .gesture(
150 | DragGesture()
151 | .onChanged { value in
152 | drawingData.shapes[index].position = value.location
153 | }
154 | )
155 |
156 | // Resize handle
157 | Rectangle()
158 | .fill(Color.blue)
159 | .frame(width: handleSize, height: handleSize)
160 | .position(x: shape.position.x + handleOffset.width, y: shape.position.y + handleOffset.height)
161 | .gesture(
162 | DragGesture()
163 | .onChanged { value in
164 | let newSize = CGSize(width: value.location.x - shape.position.x - handleSize / 2, height: value.location.y - shape.position.y - handleSize / 2)
165 | drawingData.shapes[index].size = newSize
166 | }
167 | )
168 | }
169 | }
170 | }
171 |
172 |
173 | // Render draggable text elements.
174 | ForEach(drawingData.texts.indices, id: \.self) { index in
175 | let textElement = drawingData.texts[index]
176 | Text(textElement.text)
177 | .font(.system(size: textElement.fontSize))
178 | .foregroundColor(textElement.color)
179 | .position(textElement.position)
180 | .gesture(
181 | DragGesture()
182 | .onChanged { value in
183 | drawingData.texts[index].position = value.location
184 | }
185 | )
186 | .onTapGesture {
187 | for i in 0.. Image {
491 | let renderer = UIGraphicsImageRenderer(size: size)
492 | let image = renderer.image { context in
493 | self.draw(in: CGRect(origin: .zero, size: size))
494 | }
495 | return Image(uiImage: image)
496 | }
497 | }
498 |
--------------------------------------------------------------------------------