├── 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 | ![platforms](https://img.shields.io/badge/platforms-iOS-333333.svg) 4 | [![License](https://img.shields.io/cocoapods/l/Sketch.svg?style=flat)](https://github.com/ArunaUd92/SUSketch) 5 | [![Language: Swift 5.10](https://img.shields.io/badge/swift-5.10-4BC51D.svg?style=flat)](https://developer.apple.com/swift) 6 | [![Swift Package Manager](https://img.shields.io/badge/Swift_Package_Manager-compatible-orange?style=flat-square)](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 | ![](https://github.com/ArunaUd92/SUSketch/blob/main/Images/susketch%20view.png) 11 | ![](https://github.com/ArunaUd92/SUSketch/blob/main/Images/color-picker.png) 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 | --------------------------------------------------------------------------------