Skip to content

Instantly share code, notes, and snippets.

@liquidcoder1
Created December 18, 2020 12:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save liquidcoder1/fef1a6875cf7decb8523b852f3dc66c9 to your computer and use it in GitHub Desktop.
Save liquidcoder1/fef1a6875cf7decb8523b852f3dc66c9 to your computer and use it in GitHub Desktop.
Zara replica - part 3

Hey guys, this is part 3 of the ZARA replica mobile app. Check out the previous lesson if you haven't done so already in order to follow along. So, in this one we will continue from where we left off last week .

Sizes

In the Utils folder, add the following if you don't have it already:

Sizes.swift

import SwiftUI

enum Sizes {
    
		static var navBarHeight: CGFloat { 50 }
    static var tabBarHeight: CGFloat { UIScreen.main.bounds.width * 0.15 }
    static var screenWidth = UIScreen.main.bounds.width
    static var screenHeight = UIScreen.main.bounds.height
    
}

Explanation

This is a simple helper struct that will be used in the app.

Custom TabBar View

In the Views folder, add the following:

TabbarView.swift

import SwiftUI

struct TabbarView: View {
    
    @Binding var tab: Tab?
    
    var body: some View {
        HStack {
            Button(action: {
               onTabSelected(.search)
            }, label: {
                Image(systemName: "magnifyingglass")
                    .thinFont()
                    .frame(maxWidth: .infinity)
            })
            
            Button(action: { onTabSelected(.bookmark) }, label: {
                Image(systemName: "bookmark")
                    .thinFont()
                    .frame(maxWidth: .infinity)
            })
            
            Button(action: { onTabSelected(.menu) }, label: {
                
                Text("MENU").thinFont()
                    .frame(maxWidth: .infinity)
            })
              
            Button(action: { onTabSelected(.profile) }, label: {
                Image(systemName: "person")
                    .thinFont()
                    .frame(maxWidth: .infinity)
            })
            
            Button(action: { onTabSelected(.cart) }, label: {
                Image(systemName: "bag")
                    .thinFont()
                    .overlay(
                        Text("9+").font(.system(size: 7, weight: Font.Weight.light, design: Font.Design.default))
                            .padding(.top, 2)
                        , alignment: .center)
                    .frame(maxWidth: .infinity)
            })
            
        }
        .frame(maxWidth: .infinity, maxHeight: Sizes.tabBarHeight, alignment: .top)
        .textColor()
        .bgColor()
        
    }

		func onTabSelected(_ tab: Tab)  {
        self.tab = tab
    }
}

struct TabbarView_Previews: PreviewProvider {
    static var previews: some View {
        TabbarView(tab: .constant(.bookmark))
    }
}

Explanation

The tab bar view is a horizontal stack of buttons, and in each button's action we call a onTabSelected function to set the tab binding property.

Preview

https://res.cloudinary.com/liquidcoder/image/upload/v1598168125/Zara/a75arfpmuo00c3u6bfsy.png

Product Item View

Before creating the product listing screen, we first need to create some views that will compose it. In the Views folder, add the following:

ProductItemView.swift

import SwiftUI
import KingfisherSwiftUI

struct ProductItemView: View {
    // 1
    var product: Product
    var showDetail = {}
    var bookmark = {}
    
    var body: some View {
        
        VStack {
								// 2
                KFImage(URL(string: product.mainImage)!)
                    .resizable()
                    .scaledToFill()
                    .frame(width: (Sizes.screenWidth / 2 - 25), height: 250)
                    .clipped()
                    .onTapGesture(perform: showDetail)
                    
                 // 3           
                VStack(alignment: .leading) {
                    Text(product.title)
                        .lightFont()
                        .lineLimit(1)
                    // 4
                    HStack {
                        Text(product.price)
                            .lightFont()
                        Spacer()
                        Button(action: bookmark, label: {
                            Image(systemName: "bookmark")
                                .lightFont()
                                .textColor()
                        })
                    }
                }
                
        }.padding(.horizontal)
    }
}

struct ProductItemView_Previews: PreviewProvider {
    static var previews: some View {
        ProductItemView(product: Product.men.first!)
    }
}

Explanation

  1. We first declare our properties. The showDetail closure will be called when a user taps on the product image to present the product detail view. The bookmark closure will be used for bookmarking.
  2. KFImage in a wrapper around the native swiftUI Image that we use to download and cache remote images.
  3. We put the title and the HStack into a VStack aligned to the left.
  4. The HStack contains 2 views separated by a Spacer view.

Preview

https://res.cloudinary.com/liquidcoder/image/upload/v1598182406/Zara/yomtir5hbxdtdv1ome3z.png

NavBar View

Add the following in the Views folder:

NabBarView.swift

import SwiftUI

struct NavBarView: View {
    
    var title: String
    var onReturn = {}
    var onFilter = {}

    var body: some View {
        HStack {
            Button(action: onReturn, label: {
                Image(systemName: "arrow.left")
                    .thinFont()
            })
            
            Text(title)
                .font(.system(size: 18, weight: Font.Weight.bold, design: Font.Design.default))
                .frame(maxWidth: .infinity, alignment: .center)
            
            Button(action: onFilter, label: {
                Text("FILTERS")
                    .lightFont()
            })
            
        }.padding(.horizontal)
        .padding(.bottom, 10)
        .textColor()
        .frame(height: Sizes.navBarHeight, alignment: .bottom)
        .bgColor()
    }
}

struct NavBarView_Previews: PreviewProvider {
    static var previews: some View {
        NavBarView(title: "NEW IN")
            .preferredColorScheme(.light)
    }
}

Explanation

The navbar has a title and 2 buttons, left and right. The left one will be used to return or dismiss and it will call the onReturn closure when clicked and the right one will used to filter and will call the onFilter closure when clicked.

Preview

https://res.cloudinary.com/liquidcoder/image/upload/v1598182406/Zara/sq8mluvc47pmv6rdfeuu.png

Product Listing Screen

In the Screens folder, add the following:

ProductListingScreen.swift

import SwiftUI

struct ProductListingScreen: View {
    
    
    @Binding var category: Category?
    // 1
    @EnvironmentObject private var store: Store
    
    @State private var selectedProduct: Product?

    // 2
    private let columns = [
        GridItem(.adaptive(minimum: 150), spacing: 20)
    ]
    

    var body: some View {
        
       return ZStack(alignment: .top) {
            ScrollView {
                // 3
                createGrid()
            }.padding(.top, Sizes.navBarHeight)
                .padding(.horizontal, 15)
            // 4
            createNavBar()
            
        }.onAppear(perform: {
            // 5
            setProducts()
        }).fullScreenCover(item: $selectedProduct, content: {
            ProductDetailScreen(product: $0){
				selectedProduct = nil
						}
        })
        
    }
    
    // 3
    fileprivate func createGrid() -> LazyVGrid<ForEach<[Product], String, ProductItemView>> {
        LazyVGrid(columns: self.columns, alignment: .center, spacing: 40) {
            ForEach(store.state.products) { product in
                createProductItemView(product) // 7
            }
        }
    }
    
    // 4
    fileprivate func createNavBar() -> NavBarView {
        return NavBarView(title: "NEW IN") {
            category = nil
        } onFilter: { }
    }
    
    // 5
    fileprivate func setProducts() {
        guard let cat = self.category else {
            return
        }
        switch cat {
        case .men:
            store.dispatch(.addProducts(Product.men))
        case .women:
            store.dispatch(.addProducts(Product.women))
        case .kids:
            store.dispatch(.addProducts(Product.kids))
        }
    }
    
    // 6
    fileprivate func createProductItemView(_ product: Product) -> ProductItemView {
        return ProductItemView(product: product) {
            selectedProduct = product
        } bookmark: {
            store.dispatch(.bookmark(product))
        }
    }

}

struct ProductListingScreen_Previews: PreviewProvider {
    static var previews: some View {
        ProductListingScreen(category: .constant(Category.kids)).environmentObject(Store())
    }
}

Explanation

  1. We need the store object to perform operations and retrieve products, so we get it from the environmentObject..
  2. We then declare the columns property that will define how the LazyVGrid layouts its views. Using adaptive grid items allows us to fit as many items per row as possible with 150 minimum width
  3. Inside the createGrid function, we return a LazyVGrid. The columns will be an array of adaptive GridItems. Adaptive means we fit as many items as possible in a row, but each item must have a width of no less than 150. Inside the content block, we return a ForEach block looping through the products and creating ProductItemView for each product.
  4. In the createNavBar, we return a NavBar instance initialized with a hard-coded title, but in a real world app, this would dynamic. In the onReturn closure, we set the category to nil. onFilter will stay empty.
  5. The setProducts will be called in the onAppear block of the main ZStack container. We then dispatch an addProducts action based on the unwrapped category to loads products.
  6. The createProductItemView function creates and return a ProductItemView which is the single item in the grid, it has 2 closures, the first one handles the selection of a product that will dispatch a selectProduct UIAction to the store and trigger the presentation of its detail screen, and the second one handles bookmarking that will dispatch a bookmark Action to the store.
  7. In body, we add our grid in the Scrollview, then below it we add the navbar. The fullScreenCover containing the detail screen will be presented when store.uiState.selectedProduct is not nil.
  8. Remember to add .environmentObject(Store()) in the preview for it to work .

Preview

https://res.cloudinary.com/liquidcoder/image/upload/v1598182407/Zara/deqd6zgnqjtrlb5dlq8i.png

ContentView

Now that we have all we need to implement the content view, let's do that. Add the following in the ContentView.swift.

struct ContentView: View {
    
    @EnvironmentObject private var store: Store
    @State private var presentScreen = false
    @State private var selectedTab: Tab?
    @State private var selectedCategory: Category?
    
    var body: some View {
        
        if store.state.isLoggedIn {
            createHomeScreen()
        } else {
            createLoginScreen()
        }
    }
    
    fileprivate func createLoginScreen() -> some View {
        return LoginScreen {
            store.dispatch(.login)
        }.transition(.move(edge: .bottom))
        .animation(.easeOut)
    }
    
    fileprivate func createHomeScreen() -> some View {
        
        return ZStack(alignment: .bottom){
            HomeScreen(category: $selectedCategory)
            TabbarView(tab: $selectedTab)
        }
        .fullScreenCover(isPresented: $presentScreen, content: {
            if selectedCategory != nil{
                ProductListingScreen(category: $selectedCategory)
            }

            if let tab = selectedTab {
                createTabScreen(tab)
            }
        })
        .onChange(of: selectedCategory) { category in
            presentScreen.toggle()
        }.onChange(of: selectedTab) { _ in
            presentScreen.toggle()
        }
    
    }
    
    @ViewBuilder
    private func createTabScreen(_ tab: Tab) -> some View {
        switch tab {
        case .bookmark:
            Text("Bookmark")
        case .cart:
            Text("Shopping bar")
        case .search:
            Text("Search")
        case .profile:
            Text("Profile")
        case .menu:
            Text("Menu")
        }
    }
   
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environmentObject(Store())
    }
}

Explanation

  1. We create the home screen which consist of ZStack containing the HomeScreen and TabbarView. This is also where we add the full screen cover modifier that will present product listing screen or other screen based on the selected tab bar button.
  2. We create the LoginScreen, and in its onDismiss closure, we dispatch a login action. The login functionality does not work for real though.
  3. In the presentScreen method, we present a screen based on a selected tabBar button. We haven't created those screens yet that's why we have returned placeholder Text views.
  4. The onTabSelected will be used to present other screens.

Preview

https://res.cloudinary.com/liquidcoder/image/upload/v1598182407/Zara/deqd6zgnqjtrlb5dlq8i.png

ZaraReplicaApp

To build and run the app, we need to set the store as an environmentObject. To do this, add the following in the ZaraReplicaApp struct.

To the top of the struct

private var store = Store()

And this modifier on the ContentView:

.environmentObject(store)

Your file should look like this:

import SwiftUI

@main
struct ClothingUIApp: App {
    @StateObject private var store = Store()
    
    var body: some Scene {
        WindowGroup {
            
            ContentView().environmentObject(store)
        }
    }
}

Now build and run the app. Everything should work fine... make sure you run the app on a physical device as it looks much better than on a simulator.

Conclusion

And this is lesson 3 of this mini course... stay tuned for lesson 4 which in my humble opinion will be more exciting than the last 3. So make sure you've subscribed to not miss when I publish that. Happy coding guys!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment