SwiftUI Introduction

Intro: A Shoe Designer

An app that designs shoes.

released Wed, 12 Jun 2019

In order to better understand how SwiftUI works, we will develop a simple tutorial app. We will continue adding features to this app in the next chapters. For now, the first thing we will build is a simple way for users to customize / design sneakers. In order to allow this, we need a preview of the current sneaker, a way to change the colors of the current sneaker, and a way to store the current sneaker.

Here is a small GIF of what we are about to create.

The Model

Lets start with the model as it will help us shape the rest of the application. Currently, our model will only store the colors of the current sneaker. However, future chapters will also add the manufacturers name, shoe name, shoe model, etc. So in order to prepare for that we will have a more general ShoeConfiguration type that will contain a more distinct ShoeColors type. We will first have a look at this ShoeColors type.

struct ShoeColors {

     /// Outline Color

     var outline: Color

     /// Base Color

     var base: Color

     /// Side Color

     var side: Color

     /// Sole Color

     var sole: Color

     /// Back Cage Color

     var cage: Color

}

Our configuration has colors for multiple parts of the shoe. The outline, the base color, the side color, and so on. Note that we're not using UIColor or NSColor or CGColor, or even CIColor; no, there's a new color type in SwiftUI. It has a limited set of default color defintions, but it is sufficient for our use case here.

The next part is to have a configuration for our shoe. The colors will be just one part of the configuration. A first draft would look something like this:

class ShoeConfiguration {

     

     var shoeColors: ShoeColors 

     

     init() {

         shoeColors = ShoeColors(outline: .black, base: .white, side: .orange, sole: .purple, cage: .gray)

     }

}

We're creating a simple class that acts as the configuration of one shoe. Currently, we're only hosting shoeColors, so there's not really much going on. What we do do, though, is to configure a default shoe in the initializer.

import SwiftUI

import Combine



class ShoeConfiguration: BindableObject {

     

     struct ShoeColors {

         var outline: Color

         var base: Color

         var side: Color

         var sole: Color

         var cage: Color

     }

     

     var shoeColors: ShoeColors {

         didSet {

             didChange.send(self)

         }

     }

     

     var didChange = PassthroughSubject<ShoeConfiguration, Never>()

     

     init() {

         shoeColors = ShoeColors(outline: .black, base: .white, side: .orange, sole: .purple, cage: .gray)

     }

}



struct ShoeView : View {

     

     @Binding var colors: ShoeConfiguration.ShoeColors

     

     private func colorParts() -> [(name: String, color: Color)] {

         return [

             (\"base\", colors.base),

             (\"side\", colors.side),

             (\"sole\", colors.sole),

             (\"cage\", colors.cage),

             (\"outline\", colors.outline)

         ]

     }

     

     var body: some View {

         ZStack {

             ForEach(colorParts().identified(by: \.name)) { shoePart in

                 Image(shoePart.name).resizable()

                     .renderingMode(.template)

                     .foregroundColor(shoePart.color)

             }

         }

     }

}



extension View {

     func scaledFrame(from geometry: GeometryProxy, scale: CGFloat) -> some View {

         self.frame(width: geometry.size.width * scale, height: geometry.size.height * scale)

     }

}



struct ColorPickerEntry : View {

     var selected: Bool

     var shoeColor: Color

     

     private let outerScale: CGFloat = 0.7

     private let innerScale: CGFloat = 0.5

     

     var body : some View {

         GeometryReader { geometry in

             Group {

                 if self.selected {

                     Circle().fill(self.shoeColor)

                         .overlay(Circle().stroke(Color.black, lineWidth: 2.0))

                         .scaledFrame(from: geometry, scale: self.outerScale)

                 } else {

                     Circle().stroke(Color.gray)

                         .scaledFrame(from: geometry, scale: self.innerScale)

                         .overlay(Circle().fill(self.shoeColor)

                             .scaledFrame(from: geometry, scale: self.innerScale), alignment: .center)

                         .overlay(Circle().stroke(Color.gray)

                             .scaledFrame(from: geometry, scale: self.outerScale))

                 }

             }.frame(width: geometry.size.width, height: geometry.size.height)

         }

     }

}





struct ColorPicker : View {

     @Binding var selectedColor: Color

     var name: String

     var body : some View {

         VStack(alignment: HorizontalAlignment.center, spacing: 0) {

             Text(name).font(.body)

             HStack {

                 ForEach([Color.black, Color.white, Color.orange, Color.purple, Color.gray].identified(by: \.hashValue)) { color in

                     Button(action: {

                         self.selectedColor = color

                     }) {

                         ColorPickerEntry(selected: self.selectedColor.hashValue == color.hashValue, shoeColor: color)

                             .frame(width: 38, height: 38)

                     }

                 }

             }

         }

     }

}





struct ShoeConfigurator : View {

     

     @ObjectBinding var shoeConfiguration = ShoeConfiguration()

     

     var body: some View {

         VStack {

             ShoeView(colors: $shoeConfiguration.shoeColors)

                 .frame(width: 250, height: 114, alignment: .center)

             ColorPicker(selectedColor: $shoeConfiguration.shoeColors.base,

                         name: \"Base\")

             ColorPicker(selectedColor: $shoeConfiguration.shoeColors.cage,

                         name: \"Cage\")

             ColorPicker(selectedColor: $shoeConfiguration.shoeColors.side,

                         name: \"Side\")

             ColorPicker(selectedColor: $shoeConfiguration.shoeColors.sole,

                         name: \"Sole\")

         }

     }

}



struct ContentView : View {

     @State private var selection = 0

     

     var body: some View {

         ShoeConfigurator()

     }

}



#if DEBUG

struct ContentView_Previews : PreviewProvider {

     static var previews: some View {

         ContentView()

     }

}

#endif