SwiftUI Introduction

Intro: A Shoe Designer

An app that designs shoes.

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