SwiftUI data example

With the approach of the declarative UI framework SwiftUI (the new simple way to build user interfaces on all Apple platforms), it was time to explore what all the fuss was about. During my exploration I was convinced that SwiftUI will increase the rate at which apps can be produced. There were, however, some things that did not seem immediately obvious, one of them being how @State is used. Take this perfectly valid prototype code for instance (github).

  struct StateContentView: View {
  
       private let images: [String] = ["🚀", "⛵️", "⚓️"]
  
       @State private var currentIndex: Int = 0
  
       var body: some View {
           VStack {
               Spacer()
               Text(images[currentIndex])
                   .font(.largeTitle)
               Spacer()
                   .frame(height: 16.0)
               Button(action: nextImage, label: {
                   Text("Next")
               })
               Spacer()
           }
       }
  
       private func nextImage() {
           if currentIndex < images.count - 1 {
               currentIndex += 1
           } else {
               currentIndex = 0
           }
       }
   }

This was a great way of getting something up and running quickly, however now we want to convert this code into production code where we might want the current image state to be shared by multiple views.

In UIKit we would either have stored the value in a common model or passed the current image along to the child view controller when navigating. While both options are still available to us in SwiftUI it is incredibly easy to pass along model dependencies to the view and all of its children. Step in ObservableObject.

  final class ImageModel: ObservableObject {
      ...
  }

By making our model object conform to the ObservableObject protocol we are able to pass it along as an environment object on our view when we instantiate it.

  EnvironmentObjectView().environmentObject(ImageModel())

The model will now be available to the view and all of its children by adding the @EnvironmentObject annotation to it like so

  struct EnvironmentObjectView: View {
      @EnvironmentObject var imageModel: ImageModel
      ...
  }

But the real magic comes now. By marking any properties that we want to be able to observe changes to with @Published we can now simply assign the value of those properties as data for our view and it will automatically update whenever the models value changes.

   final class ImageModel: ObservableObject {
       ...
       @Published var currentImage: String
       ...
   }
 
   struct EnvironmentObjectView: View {
  
       @EnvironmentObject var imageModel: ImageModel
  
       var body: some View {
           VStack {
               ...
               Text(imageModel.currentImage)
                   .font(.largeTitle)
               ...
               Button(action: imageModel.nextImage, label: {
                   Text("Next")
               })
               ...
           }
       }
   }

And that's it! We now have a fully reusable model that is shared between all the views. Both the views and the model are also fully testable since all dependencies have been injected through the Environment.

app preview