*BONUS ENTRY* SwiftUI Mini Project - Part 3 (final)

Christmas Day coding! Merry Christmas one and all! I may do several shorter entries today - whenever there is a good time to have a little session basically. First of all, I am going to tweak and refine the mini project. Also, I want to do a detailed breakdown of each aspect, each line of code. I need to understand that in more detail so I can understand and apply the skills. OK, let's go!

Start Time - 13:14

So, first of all, let's see what is needed to 'finish' this project...

*Paused for 15 minutes - updating all drivers' stats!

OK, so it's not easy just to add in a text box towards the top. I'll leave that for now!

The ONLY real thing to add in is to space out the text for when the driver stats come up.

Cool! Putting it within a List is the way to go. That's cool, looks MUCH better!

Spacing in between the items...harder to do than you think. The custom spacer bit works but then has an error for the final item. So I will instead put some more info in to fill the space!

*10 minutes pause for adding in years active.

Right, that will do! The driver with info bit looks MUCH better than before!

Revamping the driver page - approx 30 minutes

Now to break down the code!


In this project - again, entirely adapted from Laurie's so I'm taking NO credit for the organisation and base code - this is how it is all set out:





So this is the hierarchy of the app. It is set out with the the different Swift UI files. The main ones - relevant right now at least - are ContentView, DriverView, F1DriverListView, DriverDetailsView and Drivers. Phew!

In order of creation, as this is most logical...

Drivers

This is the class, the class which contains all of the key driver info.

struct Driver: Identifiable {
  var id = UUID()
  var name: String
  var imageName: String
    var wins: Int
    var championships: Int
    var races: Int
    var poles: Int
    var fastestLaps: Int
    var yearsActive: String
}

extension Driver {
  static let f1Drivers = [
    Driver(name: "Alain Prost", imageName: "AP", wins: 51, championships: 4, races: 199, poles: 33, fastestLaps: 41, yearsActive: "1980 - 1993"),
    Driver(name: "Ayrton Senna", imageName: "AS", wins: 41, championships: 3, races: 161, poles: 65, fastestLaps: 19, yearsActive: "1984 - 1994"),
    Driver(name: "Damon Hill", imageName: "DH", wins: 22, championships: 1, races: 115, poles: 20, fastestLaps: 19, yearsActive: "1992 - 1999"),
    Driver(name: "Fernando Alonso", imageName: "FA", wins: 32, championships: 2, races: 313, poles: 22, fastestLaps: 23,yearsActive: "2001 - 2018"),
    Driver(name: "Felipe Massa", imageName: "FM", wins: 11, championships: 0, races: 269, poles: 16, fastestLaps: 15, yearsActive: "1960 - 1994"),
    Driver(name: "Kimi Raikkonen", imageName: "KR", wins: 21, championships: 1, races: 313, poles: 18, fastestLaps: 46, yearsActive: "2001 - 2020"),
    Driver(name: "Mika Hakkinen", imageName: "MH", wins: 20, championships: 2, races: 165, poles: 26, fastestLaps: 25, yearsActive: "1991 - 2001"),
    Driver(name: "Michael Schumacher", imageName: "MS", wins: 91, championships: 7, races: 307, poles: 68, fastestLaps: 77, yearsActive: "1991 - 2012"),
    Driver(name: "Sebastian Vettel", imageName: "SV", wins: 53, championships: 4, races: 240, poles: 57, fastestLaps: 38, yearsActive: "2007 - 2020")
  ]

}

This is all totally logical. The extension bit isn't totally necessary but makes sense in terms of organisation.

The downside with using hard-built code like this and not APIs is that the current drivers will need updates after every race. Another time - look at getting API info for F1!

So, after this, it would be putting it into the ContentView, but Laurie instead wanted to keep that as clutter free as possible. So another SwiftUI file...

DriverView

struct DriverView: View {
    
    var driver: Driver
    var proxy: GeometryProxy
    
    var body: some View {
        
        VStack {
            
            Text(driver.name)
                .fontWeight(.bold)
                .font(Font.system(size: 24))
                .minimumScaleFactor(0.75)
                .padding(.top)
                .multilineTextAlignment(.center)
                .lineLimit(nil)
                .foregroundColor(.white)
            
            Image(driver.imageName)
                .renderingMode(.original)
                .resizable()
                .scaledToFit()
                .padding()
                .shadow(color: .gray, radius: 20)
            
        }
        .padding()
        .frame(width: max(proxy.size.width - proxy.frame(in: .global).midX, proxy.size.width),
               height: proxy.size.height - 50)
            
            
            .background(Image(driver.imageName)
                .renderingMode(.original)
                .resizable()
                .scaledToFill()
                .overlay(Color.gray)
                .blendMode(.multiply)
                .blur(radius: 1))
            
            
            .cornerRadius(10)
            .padding(.vertical)
            .shadow(radius: 3)
            .rotation3DEffect(Angle(degrees: Double(proxy.frame(in: .global).midX) / 40), axis: (x: -4, y: -3, z: -3))
        
    }

}

This is essentially the main display screen for drivers. Let's break this down in a bit...

Paused at 14:25 (40 minutes with interruptions etc. so far)

Continued at 21:49 - yes a lot later!

Right, let's break down the code above....

 var driverDriver
 var proxyGeometryProxy

The driver is an instance from the Driver class created. The proxy bit is something else. A definition:

OK I can't find anything that clear. But it does seem to keep everything a consistent size, in terms of position on the screen. 

var bodysome View {
        
        VStack {
            
            Text(driver.name)
                .fontWeight(.bold)
                .font(Font.system(size: 24))
                .minimumScaleFactor(0.75)
                .padding(.top)
                .multilineTextAlignment(.center)
                .lineLimit(nil)
                .foregroundColor(.white)
            
            Image(driver.imageName)
                .renderingMode(.original)
                .resizable()
                .scaledToFit()
                .padding()
                .shadow(color: .gray, radius: 20)
            
        }


OK, the var body: some View is the general start to what will go inside the main code. 
Then we have text. Inside we use the name property of the driver instance. Then we have a whole series of properties. These are fine. 
Then for Image, we have original rendering mode (otherwise it looks weird and blue), then the other bits added on. 

Then there's this - 

.padding()
        .frame(width: max(proxy.size.width - proxy.frame(in: .global).midXproxy.size.width),
               height: proxy.size.height - 50)
            
            
            .background(Image(driver.imageName)
                .renderingMode(.original)
                .resizable()
                .scaledToFill()
                .overlay(Color.gray)
                .blendMode(.multiply)
                .blur(radius: 1))
            
            
            .cornerRadius(10)
            .padding(.vertical)
            .shadow(radius: 3)
            .rotation3DEffect(Angle(degrees: Double(proxy.frame(in: .global).midX/ 40), axis: (x: -4, y: -3, z: -3))
        

OK we have some general padding around for the above two objects (text and images), then we have this size calculation! This uses a .frame property, which links back to the proxy object. The whole line of code is a complex calculation that involves the max width...I can't pretend to understand the whole thing but will play around with the size now!

Not much difference made! So, the next set of properties are for the background - the blurry bit of the driver. The grey overlay makes it look cool. Let's see blend mode and blur radius...

Nothing seems to change so I'm not sure if accessing the properties in this struct is even doing anything. Anyway, after that we have the corner radius bit, vertical padding, shadow and then the 3D rotating bit (again, using the proxy value). 

This whole bit is very technical! But it's clear that proxy is useful. Next the driver list view...

F1 Driver List View

struct F1DriverListView: View {
    
      
      @State private var isPresented = false
      @State private var selectedDriver: Driver? = nil
      
      var body: some View {
        
        NavigationView {
          ZStack {
            
           
            
            ScrollView(.horizontal, showsIndicators: false) {
              
              HStack {
                
                ForEach(Driver.f1Drivers) { driver in
                  GeometryReader { proxy in
                    
                    NavigationLink(destination: DriverDetailsView(driver: driver)) {
                      DriverView(driver: driver, proxy: proxy)
                    }
                    
                  }
                  .frame(width: 200, height: 300)
                }
              }
            }
            Spacer()
            .layoutPriority(1)
          }
          .background(Color.black)
            .edgesIgnoringSafeArea(.bottom)
            .navigationBarTitle("F1 Drivers")
            
            
        }
        
        
      }
    }

    

struct F1DriverListView_Previews: PreviewProvider {
    static var previews: some View {
        F1DriverListView()
    }
}


OK! We have the @State private vars - these are accessed within this struct. 
Then we have a NavigationView - this is where each of the drivers are put into. In a ZStack (the whole view), then a ScrollView going horizontally.
Then we have the for each loop - this then has some complex code to show driver in GeometryReader. This is this specific bit:

ForEach(Driver.f1Drivers) { driver in
                  GeometryReader { proxy in
                    
                    NavigationLink(destination: DriverDetailsView(driver: driver)) {
                      DriverView(driver: driver, proxy: proxy)
                    }

This is the code for bringing up each image of the driver - it takes us to the DriverDetailsView. 
Then we have some more properties basically. 

OK, this brings us to what is shown once each driver is clicked on...

DriverDetailsView

struct DriverDetailsView: View {
    
    var driver: Driver
    
    var body: some View {
        
        VStack {
            
            List {
        Text(driver.name)
            .font(.largeTitle)
                
                .padding()
        Image(driver.imageName)
            
           
                
        .resizable()
        .aspectRatio(contentMode: .fit)
        .frame(width: 200, height: 200)
        .padding()
            
            Text("Years Active: \(driver.yearsActive)")
                
            Text("Races: \(driver.races)")
                
       
      
             Text("Wins: \(driver.wins)")
            
             Text("Poles: \(driver.poles)")
                  
             Text("Fastest Laps: \(driver.fastestLaps)")
       

            Text("Championships: \(driver.championships)")
                
                

            }
            .padding()
          
            
        }
  
    }

}



struct DriverDetailsView_Previews: PreviewProvider {
    static var previews: some View {
        DriverDetailsView(driver: Driver.f1Drivers.randomElement()!)
    }
}

Easier to understand. There is a List, which then shows the name and image of each driver, followed by each of the properties. 

Right, next!

Content View

struct ContentView: View {
    var body: some View {
        
        F1DriverListView()
    
    }
    
}
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

Wow! Look how simple this is! It is literally the F1DriverListView, which contains all of the key info for the view. I really like that!

So, when making a project, separate SwiftUI files are needed. One for the model (the custom data types e.g. classes), then one for the main views (DriverView - has all of the properties and the proxy). We also have the navigation view that has the scroll view... Finally we have the screen that takes us to  what happens when the buttons are clicked. 

Finish Time - 22:36

(Total time: approx 1 hour 20)

So a good session breaking down the app and making sense of it. I've also got a cool list view now and have a better idea of why there are different SwiftUI files. A key takeaway is to keep the Content View REALLY simple and have the hard work done in other structs. 

Tomorrow I will do a little session on some new learning! Going to see what is on rw.com! This looks like one...
https://www.raywenderlich.com/6849561-layout-in-ios
Or this - 
https://www.raywenderlich.com/5429279-programming-in-swift-functions-and-types

Think I will start with the latter one as that was made first! 

Comments

Popular posts from this blog

*Xcode Project Entry 2* F1 Quiz - part 1

Angela Yu Course Part 10 (up to lesson 112)

Angela Yu Xcode 12 Course - Part 7 (lectures 74 to 79)