Developing drawing app with SwiftUI

Introduction

In this post, we will build a drawing app with SwiftUI. Apart from being super fun and addictive, SwiftUI with its declarative approach, makes it a lot easier and clearer to reason about the state and updates that are happening in our apps. A drawing app is a good example of this – you have multiple sources of state. For example, you need to keep track what the user has already drawn on screen. Then, you need to provide drawing controls, that should customise the drawing such as pencil size and color. A good drawing app should also have a way to revert to a previous state with an undo functionality. Or when we are not happy with our drawing at all, to start from scratch.

Handling all the complexities mentioned above can be challenging in a UIKit based app. Let’s see how we can build a simple drawing app with SwiftUI.

Implementation

Let’s create a new Xcode 11 / iOS 13 project, called DrawingPadSwiftUI. The content view, which will open up when the users are first starting the app, will consist of a title (Text view), and two custom views DrawingPad (where we will draw our masterpieces) and DrawingControls (where we will customise the drawing with color, line width and undo functionality).

Simulator Screen Shot - iPhone Xs Max - 2019-07-20 at 09.44.48.png

Translating this to SwiftUI code is pretty straightforward.

    var body: some View {
        VStack(alignment: .center) {
            Text("Draw something")
                .font(.largeTitle)
            DrawingPad(currentDrawing: $currentDrawing,
                       drawings: $drawings,
                       color: $color,
                       lineWidth: $lineWidth)
            DrawingControls(color: $color, drawings: $drawings, lineWidth: $lineWidth)
        }
    }

Now let’s have a look at the state that is passed to these views, and what that state represents.

    @State private var currentDrawing: Drawing = Drawing()
    @State private var drawings: [Drawing] = [Drawing]()
    @State private var color: Color = Color.black
    @State private var lineWidth: CGFloat = 3.0

The currentDrawing variable represents what the user is currently drawing on the screen. We need this variable in order to represent realtime what the user is currently drawing. The Drawing struct is just a simple type that contains an array of CGPoints.

struct Drawing {
    var points: [CGPoint] = [CGPoint]()
}

The drawings array contains all the drawings the user has added so far. A drawing is created when the user has started dragging on the pad and added to the array when the user has finished with the gesture.

The color @State variable, as its name implies, holds the current selected color for the drawings. The lineWidth variable determines the width of the drawing pencil.

Now, let’s examine what we are passing to the DrawingPad as a binding. The currentDrawing and drawings are passed since the drawing pad is the place where this state will change. The user will draw on the pad and based on this, these two bindings will be updated. The color and width are not going to be changed in the pad, but the view depends on these states in order to correctly display the drawing. So these values will not change in the pad, but every change of these states in other places (DrawingControls view), needs to be reflected in the drawing pad.

The DrawingControls view on the other hand, has bindings for color, lineWidth and drawings. As we mentioned above, the bindings for color and lineWidth are pretty clear – these states are updated here. The drawings array is needed because of the undo and clear functionalities. This means that the drawings state can be altered from both of the views.

Next, let’s see the DrawingPad.

     var body: some View {
        GeometryReader { geometry in
            Path { path in
                for drawing in self.drawings {
                    self.add(drawing: drawing, toPath: &path)
                }
                self.add(drawing: self.currentDrawing, toPath: &path)
            }
            .stroke(self.color, lineWidth: self.lineWidth)
                .background(Color(white: 0.95))
                .gesture(
                    DragGesture(minimumDistance: 0.1)
                        .onChanged({ (value) in
                            let currentPoint = value.location
                            if currentPoint.y >= 0
                                && currentPoint.y < geometry.size.height {
                                self.currentDrawing.points.append(currentPoint)
                            }
                        })
                        .onEnded({ (value) in
                            self.drawings.append(self.currentDrawing)
                            self.currentDrawing = Drawing()
                        })
            )
        }
        .frame(maxHeight: .infinity)
    }

Let's start from the bottom. The drawing will be done with a DragGesture, with a minimum distance of 0.1. That's the distance in points from where the detection should start. The dragGesture has two callbacks, onChanged and onEnded.

The onChanged method is called whenever there's an update of the dragging state. In this method, we are taking the current location of the event and we are checking if it's in the bounds of the drawing pad. Detecting the start of the pad is pretty straightforward, we are just checking if the point's Y value is bigger than zero. But how do we get the size of the drawing pad?

Enter the GeometryReader. This view returns a flexible preferred size to its parent layout. Or in simpler words, it tells the parent view (the container view in our case), what size it should be. It also provides access to the geometry of the view, which contains its size. To better understand how this works, please have a look at the amazing WWDC session Building Custom Views with Swift UI. Going back to our gesture, we will use the size of the geometry to get the height of the view and we will limit the points added to the drawing to that height.

The onEnded method is called when the dragging ends. In this method, we are adding the finished drawing to our list of drawings and we clear out the currentDrawing.

We have seen how to collect the drawings with a dragging gesture. Next, let’s see how we can display them, by looking at the Path¬†component. It’s similar to UIBezierPath from UIKit. Basically, we provide points to the path and we draw different shapes with those points (lines, curves, rectangles etc). In our case, we want to draw lines between two points (starting and current point of dragging).

As we have seen above, all variables are passed from the parent view as @Binding, which means every update in this view will automatically be reflected in the parent view and other views that are depending on that state.

Next, let’s have a look at the DrawingControls view.

    var body: some View {
        NavigationView {
            VStack {
                HStack(spacing: spacing) {
                    Button("Pick color") {
                        self.colorPickerShown = true
                    }
                    Button("Undo") {
                        if self.drawings.count > 0 {
                            self.drawings.removeLast()
                        }
                    }
                    Button("Clear") {
                        self.drawings = [Drawing]()
                    }
                }
                HStack {
                    Text("Pencil width")
                        .padding()
                    Slider(value: $lineWidth, from: 1.0, through: 15.0, by: 1.0)
                        .padding()
                }
            }

        }
        .frame(height: 200)
        .sheet(isPresented: $colorPickerShown, onDismiss: {
            self.colorPickerShown = false
        }, content: { () -> ColorPicker in
            ColorPicker(color: self.$color, colorPickerShown: self.$colorPickerShown)
        })
    }

At the root of this view we have a NavigationView, because we will need to present the color selection sheet. Next, we have a vertical stack, that consists of a horizontal stack of three buttons (Pick color, Undo and Clear) and another horizontal stack that has a text and a slider for the selection of the pencil width.

One interesting thing here is the presentation of a view controller. First, we need to define a new @State boolean variable, colorPickerShown, which determines whether the picker should be shown. We set this variable to true on the click of the “Pick color” button. Next, we add a .sheet() modifier to the navigation view, which expects a boolean for its isPresented state. We pass the colorPickerShown here. On dismiss of this sheet, we set the colorPickerShown value to false. In the content callback, we provide a ColorPicker view, which has two bindings, for the color and for the colorPickerShown state.

The color picker contains a list of colors retrieved from a ColorsProvider. Whenever an entry of the list is tapped, that color is selected in the drawing controls and the modal screen is dismissed.

struct ColorPicker: View {
    @Binding var color: Color
    @Binding var colorPickerShown: Bool

    private let colors = ColorsProvider.supportedColors()

    var body: some View {
        List(colors) { colorInfo in
            ColorEntry(colorInfo: colorInfo).tapAction {
                self.color = colorInfo.color
                self.colorPickerShown = false
            }
        }
    }
}

To represent a color entry, we have a new view, which consists of a circle filled with the appropriate color and its display name.

struct ColorEntry: View {
    let colorInfo: ColorInfo

    var body: some View {
        HStack {
            Circle()
                .fill(colorInfo.color)
                .frame(width: 40, height: 40)
                .padding(.all)
            Text(colorInfo.displayName)
        }
    }
}

That’s everything that needs to be done in order to have a basic drawing app in SwiftUI.

Conclusion

This sample project is a good exercise on understanding how to model state in SwiftUI. The benefits of using this declarative approach are that we can hardly have inconsistent state. We don’t have to worry about calling update functions whenever something has changed. We just setup the appropriate bindings and let SwiftUI take care of the rest.

The complete source code project can be found here.

 

Advertisements

About martinmitrevski

Software Engineer | Book Author | International Speaker. I love exploring and building things.

1 Response

  1. I see you don’t monetize martinmitrevski.com, don’t waste your traffic, you can earn additional cash every month
    with new monetization method. This is the best adsense alternative for any type
    of website (they approve all websites), for more info simply search in gooogle: murgrabia’s tools

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s