Design+Code logo

Quick links

Suggested search

Create your Model

Before everything, we need to create our data model first. We will making an HTTP request to the https://random-data-api.com/api/coffee/random_coffee?size=30 endpoint to get 30 random coffees. If you click on the link, you'll see an array of objects, and each object represents a coffee.

Screen Shot 2021-04-09 at 11.20.29 AM

Note: I'm using a Chrome extension called JSON Formatter to format how the JSON is displayed in my browser. If you don't have the extension, you can get it in the Chrome Web Store.

Let's take a coffee object as an example and create our coffee model:

// Coffee.swift

import Foundation

struct Coffee: Identifiable, Decodable {
    var id: Int
    var uid: String
    var blend_name: String
    var origin: String
    var variety: String
    var notes: String
    var intensifier: String
}

extension Coffee {
    var blendName: String { return blend_name }
}

Each coffee has an ID, a UID, a blend name, an origin, a variety, some notes and an intensifier.

In the JSON, the blend name is specified as blend_name. We must conform our model's variables to exactly the same as the JSON, so that the JSON decoder works. However, in Swift, the convention is to always use [CamelCase](https://en.wikipedia.org/wiki/Camel_case#:~:text=Camel%20case%20(sometimes%20stylized%20as,word%20starting%20with%20either%20case.) and not snake_case, so we're creating an extension on Coffee, adding the blendName variable, and returning the blend_name. Therefore, we'll be able to call coffee.blendName in our View.

Create a ViewModel

Now that we got our Coffee model, we can create our ViewModel. The ViewModel will contain all the functions related to the data fetching, processing and filtering. Create a new file named CoffeeViewModel.swift. This file will contain a class that will conform to the ObservableObject protocol.

// CoffeeViewModel.swift

import Foundation

final class CoffeeViewModel: ObservableObject {

}

Make the HTTP request

Create a variable allCoffees, where we'll store all the coffees we will get from our HTTP request. Also, create a @Published filteredCoffees variable. This is the variable we'll be using to display our coffees in the UI, and also the variable that we'll be using to filter the coffee when the user is searching.

// CoffeeViewModel.swift

var allCoffees: [Coffee] = []
@Published var filteredCoffees: [Coffee] = []

Next, create your fetchCoffees function. Read the section HTTP Request to learn more about how to create an HTTP get function in SwiftUI.

We'll be saving the response's data into the allCoffees variable. Initially, the filteredCoffees array will contains allCoffees, so we'll simply assign filteredCoffees to allCoffees.

// CoffeeViewModel.swift

func fetchCoffees() {
    guard let url = URL(string: "https://random-data-api.com/api/coffee/random_coffee?size=20") else { fatalError("Missing URL") }

    let urlRequest = URLRequest(url: url)

    let dataTask = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
        if let error = error {
            print("Request error: ", error)
            return
        }

        guard let response = response as? HTTPURLResponse else { return }

        if response.statusCode == 200 {
            guard let data = data else { return }
            DispatchQueue.main.async {
                do {
                    self.allCoffees = try JSONDecoder().decode([Coffee].self, from: data)
                    self.filteredCoffees = self.allCoffees
                } catch let error {
                    print("Error decoding: ", error)
                }
            }
        }
    }

    dataTask.resume()
}

Then, call your fetchCoffees functions on initialize of the CoffeeViewModel:

// CoffeeViewModel.swift

init() {
    fetchCoffees()
}

Add the environmentObject

Now it's time to link our CoffeeViewModel to our app! In ProjectNameApp.swift, initialize your ViewModel and attach it to your ContentView as an environment object:

// ProjectNameApp.swift

import SwiftUI

@main
struct SearchApp: App {
    var coffeeVM = CoffeeViewModel() // Initialize your ViewModel

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environmentObject(coffeeVM) // Attach it as an environmentObject
        }
    }
}

In ContentView, add the environmentObject at the top of the structure.

// ContentView.swift

struct ContentView: View {
    @EnvironmentObject var coffeeVM: CoffeeViewModel

		// More code...
}

Don't forget to also initialize the CoffeeViewModel in your preview to make it work.

// ContentView.swift's preview

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

Display the data

Display the data in your view. Create a CoffeeRow that takes a coffee as an argument and displays its blendName, origin and notes.

// CoffeeRow.swift

struct CoffeeRow: View {
    var coffee: Coffee

    var body: some View {
        VStack(alignment: .leading) {
            HStack(alignment: .top) {
                Text("☕️")
                    .font(.title)
                    .padding(.trailing, 12)

                VStack(alignment: .leading) {
                    Text(coffee.blendName)
                        .bold()

                    VStack(alignment: .leading) {
                        Text("From \(coffee.origin)")
                        Text(coffee.notes).italic()

                    }
                    .font(.caption)
                }
            }
            Divider().padding(.bottom, 12).padding(.top, 8)
        }
        .padding(.horizontal, 12)
        .frame(maxWidth: .infinity, alignment: .leading)
        .padding(12.0)
    }
}

You might run into an error when trying to build for the preview. To make it work, head over to the Data from JSON section of this SwiftUI Handbook to learn how to load local data from a JSON file into your SwiftUI application.

In ContentView, create a ScrollView and iterate over the filteredCoffees from the CoffeeViewModel.

ScrollView {
    ForEach(coffeeVM.filteredCoffees) { coffee in
        CoffeeRow(coffee: coffee)
            .padding(.horizontal)
    }
}
.padding(.horizontal)

Add the search feature

Next, we'll add the search feature in our CoffeeViewModel. Add a @Published searchText variable at the top of the class.

// CoffeeViewModel.swift

@Published var searchText = ""

Create your filterContent function. This function will check for all the matching coffees in the allCoffees variable, and return the matching coffees if the searchText the user inputs is higher than one character. Otherwise, the function will return all the coffees available.

func filterContent() {
    let lowercasedSearchText = searchText.lowercased()

    if searchText.count > 1 {
        var matchingCoffees: [Coffee] = []

        allCoffees.forEach { coffee in
            let searchContent = coffee.blendName + coffee.intensifier + coffee.notes + coffee.origin + coffee.variety
            if searchContent.lowercased().range(of: lowercasedSearchText, options: .regularExpression) != nil {
                matchingCoffees.append(coffee)
            }
        }

        self.filteredCoffees = matchingCoffees

    } else {
        filteredCoffees = allCoffees
    }
}

Create your search input

Create a TextField. The bound value of the TextField will be the @Published searchText from our CoffeeViewModel.

VStack(alignment: .leading) {
    TextField("Search", text: $coffeeVM.searchText)
        .font(.body)
        .padding()
        .background(Color(#colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 0.2662717301)))
        .cornerRadius(20)
        .onChange(of: coffeeVM.searchText) { text in
            coffeeVM.filterContent()
        }
}
.padding(.horizontal, 50)

On change of the searchText, we'll call the filterContent function we created in CoffeeViewModel. The changes in the filteredCoffees array will automatically be reflected in our view, because the filteredCoffees variable is @Published.

Add a condition

In your ScrollView, add a condition to only iterate over filteredCoffees if the array contains one or more items. If not, we'll display a Text that says No coffee found.

ScrollView {
    if coffeeVM.filteredCoffees.count > 0 {
        ForEach(coffeeVM.filteredCoffees) { coffee in
            CoffeeRow(coffee: coffee)
                .padding(.horizontal)
        }
    } else {
        Text("No coffee found 😥")
    }

}
.padding(.horizontal)

Congratulations! You can now test your coffee application by running it in the Simulator! If you wish to test in the preview, remember to press the play button in order to fetch the data from the API. The final code for the CoffeeViewModel and ContentView can be found below.

Final code

The final code for CoffeeViewModel is:

import Foundation

final class CoffeeViewModel: ObservableObject {
    @Published var searchText = ""
    @Published var filteredCoffees: [Coffee] = []
    var allCoffees: [Coffee] = []

    init() {
        fetchCoffees()
    }

    func fetchCoffees() {
        guard let url = URL(string: "https://random-data-api.com/api/coffee/random_coffee?size=20") else { fatalError("Missing URL") }

        let urlRequest = URLRequest(url: url)

        let dataTask = URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
            if let error = error {
                print("Request error: ", error)
                return
            }

            guard let response = response as? HTTPURLResponse else { return }

            if response.statusCode == 200 {
                guard let data = data else { return }
                DispatchQueue.main.async {
                    do {
                        self.allCoffees = try JSONDecoder().decode([Coffee].self, from: data)
                        self.filteredCoffees = self.allCoffees
                    } catch let error {
                        print("Error decoding: ", error)
                    }
                }
            }
        }

        dataTask.resume()
    }

    func filterContent() {
        let keywordRegex = "\\b(\\w*" + searchText.lowercased() + "\\w*)\\b"

        if searchText.count > 1 {
            var matchingCoffees: [Coffee] = []
            allCoffees.forEach { coffee in
                let searchContent = coffee.blendName + coffee.intensifier + coffee.notes + coffee.origin + coffee.variety
                if searchContent.lowercased().range(of: keywordRegex, options: .regularExpression) != nil {
                    matchingCoffees.append(coffee)
                }
            }
            self.filteredCoffees = matchingCoffees
        } else {
            filteredCoffees = allCoffees
        }
    }

}

The final code for ContentView is:

struct ContentView: View {
    @EnvironmentObject var coffeeVM: CoffeeViewModel

    var body: some View {
        VStack(spacing: 30) {
            VStack(alignment: .leading) {
                Text("Coffees")
                    .font(.largeTitle)
                    .fontWeight(.bold)

                Text("Browse our selection of the finest coffees")
            }

            VStack(alignment: .leading) {
                TextField("Search", text: $coffeeVM.searchText)
                    .font(.body)
                    .padding()
                    .background(Color(#colorLiteral(red: 0.8039215803, green: 0.8039215803, blue: 0.8039215803, alpha: 0.2662717301)))
                    .cornerRadius(20)
                    .onChange(of: coffeeVM.searchText) { text in
                        coffeeVM.filterContent()
                    }
            }
            .padding(.horizontal, 50)

            ScrollView {
                if coffeeVM.filteredCoffees.count > 0 {
                    ForEach(coffeeVM.filteredCoffees) { coffee in
                        CoffeeRow(coffee: coffee)
                            .padding(.horizontal)
                    }
                } else {
                    Text("No coffee found 😥")
                }

            }
            .padding(.horizontal)
        }
    }
}

The final result for the coffee application looks like this, in dark mode:

Screen Shot 2021-04-09 at 12.21.24 PM

Learn with videos and source files. Available to Pro subscribers only.

Purchase includes access to 50+ courses, 320+ premium tutorials, 300+ hours of videos, source files and certificates.

BACK TO

Firebase Storage

READ NEXT

Push Notifications Part 1

Templates and source code

Download source files

Download the videos and assets to refer and learn offline without interuption.

check

Design template

check

Source code for all sections

check

Video files, ePub and subtitles

Videos

Assets

ePub

Subtitles

1

Firebase Auth

How to install Firebase authentification to your Xcode project

8:18

2

Read from Firestore

Install Cloud Firestore in your application to fetch and read data from a collection

8:01

3

Write to Firestore

Save the data users input in your application in a Firestore collection

5:35

4

Join an Array of Strings

Turn your array into a serialized String

3:33

5

Data from JSON

Load data from a JSON file into your SwiftUI application

5:08

6

HTTP Request

Create an HTTP Get Request to fetch data from an API

6:31

7

WKWebView

Integrate an HTML page into your SwiftUI application using WKWebView and by converting Markdown into HTML

5:25

8

Code Highlighting in a WebView

Use Highlight.js to convert your code blocks into beautiful highlighted code in a WebView

5:11

9

Test for Production in the Simulator

Build your app on Release scheme to test for production

1:43

10

Debug Performance in a WebView

Enable Safari's WebInspector to debug the performance of a WebView in your application

1:57

11

Debug a Crash Log

Learn how to debug a crash log from App Store Connect in Xcode

2:22

12

Simulate a Bad Network

Test your SwiftUI application by simulating a bad network connection with Network Link Conditionner

2:11

13

Archive a Build in Xcode

Archive a build for beta testing or to release in the App Store

1:28

14

Apollo GraphQL Part I

Install Apollo GraphQL in your project to fetch data from an API

6:21

15

Apollo GraphQL Part 2

Make a network call to fetch your data and process it into your own data type

6:43

16

Apollo GraphQL Part 3

Display the data fetched with Apollo GraphQL in your View

5:08

17

Configuration Files in Xcode

Create configuration files and add variables depending on the environment - development or production

4:35

18

App Review

Request an app review from your user for the AppStore

5:43

19

ImagePicker

Create an ImagePicker to choose a photo from the library or take a photo from the camera

5:06

20

Compress a UIImage

Compress a UIImage by converting it to JPEG, reducing its size and quality

3:32

21

Firebase Storage

Upload, delete and list files in Firebase Storage

11:11

22

Search Feature

Implement a search feature to filter through your content in your SwiftUI application

9:13

23

Push Notifications Part 1

Set up Firebase Cloud Messaging as a provider server to send push notifications to your users

5:59

24

Push Notifications Part 2

Create an AppDelegate to ask permission to send push notifications using Apple Push Notifications service and Firebase Cloud Messaging

6:30

25

Push Notifications Part 3

Tie everything together and test your push notifications feature in production

6:13

26

Network Connection

Verify the network connection of your user to perform tasks depending on their network's reachability

6:49

27

Download Files Locally Part 1

Download videos and files locally so users can watch them offline

6:05

28

Download Files Locally Part 2

Learn how to use the DownloadManager class in your views for offline video viewing

6:02

29

Offline Data with Realm

Save your SwiftUI data into a Realm so users can access them offline

10:20

30

HTTP Request with Async Await

Create an HTTP get request function using async await

6:11

31

Xcode Cloud

Automate workflows with Xcode Cloud

9:23

32

SceneStorage and TabView

Use @SceneStorage with TabView for better user experience on iPad

3:52

33

Network Connection Observer

Observe the network connection state using NWPathMonitor

4:37

34

Apollo GraphQL Caching

Cache data for offline availability with Apollo GraphQL

9:42

35

Create a model from an API response

Learn how to create a SwiftUI model out of the response body of an API

5:37

36

Multiple type variables in Swift

Make your models conform to the same protocol to create multiple type variables

4:23

37

Parsing Data with SwiftyJSON

Make API calls and easily parse data with this JSON package

9:36

38

ShazamKit

Build a simple Shazam clone and perform music recognition

12:38

39

Firebase Remote Config

Deliver changes to your app on the fly remotely

9:05