GeometryRender in SwiftUI

Sure, sometimes when developing mobile applications, you find yourself in situations where you want some elements to have dimensions proportional to the screen. In such cases, GeometryReader is what you need.

With GeometryReader, it’s possible to assign sizes to our elements relative to their containers.

Let’s look at an example. Suppose we want to create a semaphore where each circle occupies a third of the screen width, like this

The code:

struct ContentView: View {
    var body: some View {
        GeometryReader { geometry in
            VStack {
                Circle().frame(height: geometry.size.height * 0.3).foregroundStyle(.green)
                Circle().frame(height: geometry.size.height * 0.3).foregroundStyle(.yellow)
                Circle().frame(height: geometry.size.height * 0.3).foregroundStyle(.red)
            }.frame(width: geometry.size.width)
            
        }
    }
}

Thus, for each circle, we set the height to 30% of the GeometryReader’s area (in this case, the screen) and set the size of the VStack to match the width of the render. If we omit this setting, the circles would be displayed on the left because the width of the VStack would default to the width of the circles.

As mentioned at the beginning of this post, GeometryReader is excellent for setting the sizes of our elements relative to their containers. Now, let’s see how to create a circle with its height being 30% of a VStack that has a size of 300×300:

struct ContentView: View {
    var body: some View {
        VStack {
            GeometryReader { geometry in
                VStack {
                    Circle().frame(height: geometry.size.height * 0.3).foregroundStyle(.green)
                    Circle().frame(height: geometry.size.height * 0.3).foregroundStyle(.yellow)
                    Circle().frame(height: geometry.size.height * 0.3).foregroundStyle(.red)
                }.frame(width: 300)
            }
        }.frame(width: 300, height: 300)
    }
}

to have:

Scan QR code in SwiftUI

If your app requires QR code scanning capabilities, or if you’re simply interested in this topic, you might find this post useful.

First, we’ll craft the primary view, integrating a toolbar button that unveils a sheet dedicated to scanning, with the QR code displayed centrally upon detection.

Subsequently, we’ll delineate the sheet featuring a closure mechanism, ensuring it automatically dismisses upon QR code identification.

Lastly, we’ll delve into the crux of the matter: the scanning process itself.

First thing we have to add to the permission the Privacy – Camera Usage Description otherwise the app will not start.

Introducing the ContentView:

struct ContentView: View {
    @State var qr = ""
    @State var scan = false
    
    var body: some View {
        NavigationStack {
            VStack {
                Text(qr)
            }.sheet(isPresented: $scan) {
                SheetView(qrCode: $qr)
            }
            .padding()
            .toolbar {
                Button(action: {
                    qr = ""
                    scan = true
                }) {
                    Image(systemName: "qrcode.viewfinder")
                }
            }
        }
    }
}

Thus, the SheetView is activated when the scan button is pressed, and any QR code scanned is stored in the QR variable.

Here’s a closer look at the SheetView:

import SwiftUI

struct SheetView: View {
    @Environment(\.dismiss) private var dismiss
    @Binding var qrCode: String
    var body: some View {
        VStack {
            QrScannerView(qr: $qrCode)
        }.overlay(
            VStack {
                Spacer()
                Button("Close") {
                    dismiss()
                }.foregroundStyle(.black)
                    .frame(maxWidth: 60, maxHeight: 60)
                    .background(.yellow)
                    .cornerRadius(10)
            }
        ).onChange(of: qrCode) {
            dismiss()
        }
    }
}

#Preview {
    @State var dum = ""
    return SheetView(qrCode: $dum)
}

This view incorporates the QrScannerView, which stands as the cornerstone of the application, alongside a button for closing the view. Upon detecting a QR code, the sheet is automatically dismissed via the onChange mechanism.

Now, onto the crucial part:

import SwiftUI
import VisionKit

@MainActor
struct QrScannerView: UIViewControllerRepresentable {
    @Binding var qr: String
   
    var scannerViewController: DataScannerViewController = DataScannerViewController(
        recognizedDataTypes: [.barcode()],
        qualityLevel: .accurate,
        recognizesMultipleItems: false,
        isHighFrameRateTrackingEnabled: false,
        isHighlightingEnabled: false
    )
    
    var scannerAvailable: Bool {
        DataScannerViewController.isSupported &&
        DataScannerViewController.isAvailable
    }
    
    
    func makeUIViewController(context: Context) -> DataScannerViewController {
        scannerViewController.delegate = context.coordinator
        return scannerViewController
    }
    
    func updateUIViewController(_ uiViewController: DataScannerViewController, context: Context) {
    }
    
    func makeCoordinator() -> Coordinator {
        return Coordinator(self)
    }
        
    class Coordinator: NSObject, DataScannerViewControllerDelegate {
        var parent: QrScannerView
        var qr = ""
        init(_ parent: QrScannerView) {
            self.parent = parent
            parent.start()

        }
        
        func dataScanner(_ dataScanner: DataScannerViewController, didAdd addedItems: [RecognizedItem], allItems: [RecognizedItem]) {
            processAddedItems(items: addedItems)
        }
        
        func dataScanner(_ dataScanner: DataScannerViewController, didRemove removedItems: [RecognizedItem], allItems: [RecognizedItem]) {
        }
        
        func dataScanner(_ dataScanner: DataScannerViewController, didUpdate updatedItems: [RecognizedItem], allItems: [RecognizedItem]) {
        }
        
        func dataScanner(_ dataScanner: DataScannerViewController, didTapOn item: RecognizedItem) {
            processItem(item: item)
        }
        
        
        func processAddedItems(items: [RecognizedItem]) {
            for item in items {
                processItem(item: item)
            }
        }

        func processItem(item: RecognizedItem) {
            switch item {
            case .barcode(let code):
                parent.qr = code.payloadStringValue ?? ""
            case .text(_):
                break
            @unknown default:
                print("Should not happen")
            }
        }
    }
}

To delve deeper, it’s essential to highlight a few key points, starting with the importation of VisionKit. This toolkit empowers developers to scan not only QR codes and barcodes but also text.

The view is annotated with @MainActor, indicating it executes on the main thread, ensuring the UI remains responsive and updated in real-time.

It implements the UIViewControllerRepresentable protocol, facilitating the integration of a UIViewController within SwiftUI. This approach enables SwiftUI views to manage traditional UIKit controllers seamlessly.

The qr variable is designated to store the string extracted from the QR code, serving as a bridge for data retrieved from scans.

scannerViewController encapsulates the scanner controller, initialized with specific properties tailored for barcode scanning, although it’s also capable of scanning text.

scannerAvailable assesses the availability of the scanner (and by extension, the camera), ensuring the device can perform the required operations.

updateUIViewController is responsible for configuring the controller post-initialization, while makeController initializes and returns the controller, setting the stage for its operation.

The controller adheres to the DataScannerControllerViewDelegate protocol, which is defined as an object that analyzes live video from the camera for text, data within text, and machine-readable codes, essentially handling the intricate aspects of scanning.

Scanning starts automatically upon the controller’s initialization, with a call to start from the parent object, streamlining the process for immediate action.

processItem meticulously analyzes discovered items; if an item is identified as a QR code, its string value is captured and processed accordingly.

The use of @unknown default in Swift provides a safeguard against potential incompatibilities that might arise in the future, ensuring the application remains robust across updates and changes in the development landscape.

The code https://github.com/niqt/swift-qr-code

Note: English is not my native language, so I apologize for any errors. I use AI solely to generate the banner of the post; the content is human-generated.

To subscribe to my newsletter [https://nicoladefilippo.com/#mailinglist]

Table in SwiftUI (Episode III° – The Split)

When you use the app on an iPad, you often see a list on the left and some details on the right. In this post, we will explore how to implement this layout.

Our starting point is the code from the first post in this series, which you can find in ContentView.swift.

The first change we’ll make is to create our TableView struct::

struct TableView: View {
    @State var beers: [Beer] = []
    @State var page = 0
    @State private var sortOrder = [KeyPathComparator(\Beer.name, order: .reverse)]
    @State private var searchText = ""
    @State private var beerSelected: Set<Beer.ID> = Set<Beer.ID>()
    var body: some View {
        Table(beersSearched, selection: $beerSelected, sortOrder: $sortOrder) {
            TableColumn("Name", value: \.name)
            TableColumn("First Brew", value: \.first_brewed)
            TableColumn("ABV") { beer in
                Text("\(beer.abv)")
            }
            TableColumn("Image") { beer in
                AsyncImage(url: URL(string: beer.image_url)) { image in
                    image.resizable().scaledToFit()
                } placeholder: {
                    ProgressView()
                }
                .frame(width: 100, height: 100)
            }
        }.onAppear {
            Task {
                await getBeers()
            }
        }.onChange(of: sortOrder) {
            beers.sort(using: sortOrder)
        }.searchable(text: $searchText, prompt: Text("Search by name"))
    }
    
    var beersSearched: [Beer] {
        if searchText.count == 0 {
            return self.beers
        } else {
            return self.beers.filter({(beer: Beer) -> Bool in
                return beer.name.lowercased().contains(searchText.lowercased())
            })
        }
    }
    
    func getBeers() async {
        do {
            page += 1
            let url = URL(string: "https://api.punkapi.com/v2/beers?page=\(page)&per_page=30")!
            let (data, _) = try await URLSession.shared.data(from: url)
            let beersLoaded = try JSONDecoder().decode([Beer].self, from: data)
            beers = beersLoaded + beers
            beers.sort(using: sortOrder)
        } catch {
            print("Some error")
        }
    }
}

This approach allows us to reuse the code as follows:

struct ContentView: View {
    var body: some View {
        NavigationSplitView(sidebar: {
            TableView()
        }, detail: {
            TableView()
        })
    }
}

With the NavigationSplitView, there is an element on the left (the sidebar) and the rest of the space is dedicated to the detail view. If we insert a table in the sidebar, only the first column will be visible:

With this code, the same view is presented in both the sidebar and the details section. As an exercise, you can try to create your own custom detail view.

Here is one possible solution:

struct TableView: View {
    @State var beers: [Beer] = []
    @State var page = 0
    @State private var sortOrder = [KeyPathComparator(\Beer.name, order: .reverse)]
    @State private var searchText = ""
    @Binding var beerSelected: Beer?
    @State var idSelected: Beer.ID?
    
    var body: some View {
        Table(beersSearched, selection: $idSelected, sortOrder: $sortOrder) {
            TableColumn("Name", value: \.name)
            TableColumn("First Brew", value: \.first_brewed)
            TableColumn("ABV") { beer in
                Text("\(beer.abv)")
            }
            TableColumn("Image") { beer in
                AsyncImage(url: URL(string: beer.image_url)) { image in
                    image.resizable().scaledToFit()
                } placeholder: {
                    ProgressView()
                }
                .frame(width: 100, height: 100)
            }
        }.onAppear {
            Task {
                await getBeers()
            }
        }.onChange(of: sortOrder) {
            beers.sort(using: sortOrder)
        }.searchable(text: $searchText, prompt: Text("Search by name"))
        .onChange(of: idSelected) { // <-- New
            beerSelected = beers.first(where: {(beer) in beer.id == idSelected!})
        }
    }
    
    // the rest of the code is equal
}

I made a few minor adjustments:

  • The beerSelected variable is now an optional of type Beer? This variable will hold the selected beer and is bound to the variable passed from ContentView.
  • idSelected contains the ID of the selected beer.
  • I added a new onChange method, which searches for the selected beer by its ID. I’m aware it’s not the best approach, but for this purpose, it’s sufficient.

The ContentView is now updated as follows:

struct ContentView: View {
    @State private var beerSelected: Beer?

    var body: some View {
        NavigationSplitView(sidebar: {
            TableView(beerSelected: $beerSelected)
        }, detail: {
            if let selected = beerSelected {
                VStack {
                    AsyncImage(url: URL(string: selected.image_url)) { image in
                        image.resizable().scaledToFit()
                    } placeholder: {
                        ProgressView()
                    }
                    .frame(width: 200, height: 200)
                    Text("\(selected.name)")
                }
            } else {
                Text("Please Select")
            }
        })
    }
}

In the ContentView, we declare beerSelected which is passed to the TableView. When it’s not null, we display the name and the image of the beer.

One last thing, is possible change the width of the column, using the property width.

With this post, the series is complete. Stay tuned for new posts.

Note: English is not my native language, so I apologize for any errors. I use AI solely to generate the banner of the post; the content is human-generated.

Table in SwifUI (II° Episode, search and select).

When using a table, you typically manage a lot of data, so some basic operations include searching and selecting. In this post, you’ll learn how to search for data in a table and how to select a row.

Search

Before we start, consider that our starting point is the code from Episode I, which you can obtain here .

The first step is to add the search functionality. To do this, we need to incorporate a NavigationStack into our code:

NavigationStack {
            Table(foundBeers, sortOrder: $sortOrder) {
                // the same code
            }.onAppear {
                Task {
                    await getBeers()
                }
            }.onChange(of: sortOrder) {
                beers.sort(using: sortOrder)
            }.searchable(text: $searchText, prompt: Text("Search by name"))
        }

Therefore, we added the searchable modifier (similar to how it’s done for a list). The searchText will contain the text entered by the user, and foundBeers will hold the results of the search.

var foundBeers: [Beer] {
        if searchText.count == 0 {
            return self.beers
        } else {
            return self.beers.filter({(beer: Beer) -> Bool in
                return beer.name.lowercased().contains(searchText.lowercased())
            })
        }
    }

If the search field is empty, this variable contains all the beers. Otherwise, it filters by name. The filter function retrieves all the beers that match the condition where the name contains the searchText – all in lowercase to avoid case sensitivity.

Combining the pieces:

struct ContentView: View {
    @State var beers: [Beer] = []
    @State var page = 0
    @State private var sortOrder = [KeyPathComparator(\Beer.name, order: .reverse)]
    @State private var searchText = ""

    var body: some View {
        NavigationStack {
            Table(foundBeers, sortOrder: $sortOrder) {
                TableColumn("Name", value: \.name)
                TableColumn("First Brew", value: \.first_brewed)
                TableColumn("ABV") { beer in
                    Text("\(beer.abv)")
                }
                TableColumn("Image") { beer in
                    AsyncImage(url: URL(string: beer.image_url)) { image in
                        image.resizable().scaledToFit()
                    } placeholder: {
                        ProgressView()
                    }
                    .frame(width: 100, height: 100)
                }
            }.onAppear {
                Task {
                    await getBeers()
                }
            }.onChange(of: sortOrder) {
                beers.sort(using: sortOrder)
            }.searchable(text: $searchText, prompt: Text("Search by name"))
        }
    }
    var foundBeers: [Beer] {
        if searchText.count == 0 {
            return self.beers
        } else {
            return self.beers.filter({(beer: Beer) -> Bool in
                return beer.name.lowercased().contains(searchText.lowercased())
            })
        }
    }
    
    func getBeers() async {
        // get from previous
    }
}

Select

To select and highlight an item, we need to make some minor changes:

  1. Add a variable that will contain the selected item or items.
  2. Use a different initialization function for the table.

For the first point:

@State private var beerSelected: Beer.ID?

Considering that there might be a selection or not, the beerSelected variable is optional (based on the identifiable property). If you want to enable multiple selections, replace Beer.ID? with an empty Set<Beer.ID>.

@State private var beerSelected: Set<Beer.ID> = Set<Beer.ID>()

For the second point on the list:

Table(beersSearched, selection: $beerSelected, sortOrder: $sortOrder) {

Please select the right beer!

Note: English is not my native language, so I apologize for any errors. I use AI solely to generate the banner of the post; the content is human-generated.

Table with SwiftUI (Episode I°)

If a list is a must-have for the iPhone, similarly, the Table can be essential for the iPad. Typically, when developing applications for larger screens, such as iPads or computers, it’s advantageous to be familiar with this component.

This post is the first in a series about Table.

For this post, I’d like to start with the code from this post https://nicoladefilippo.com/async-image-in-swiftui/, so we don’t reinvent the wheel and can focus solely on the relevant part.

Just to cover the basics, we retrieve a list of beers from here with this code:

struct ContentView: View {
    @State var beers: [Beer] = []
    @State var page = 0
    
    var body: some View {
        List(beers) { beer in
            HStack {
                AsyncImage(url: URL(string: beer.image_url)) { image in
                    image.resizable().scaledToFit()
                } placeholder: {
                    ProgressView()
                }.frame(width: 50, height: 50)
                Text(beer.name)
            }
        }.onAppear {
            Task {
                await getBeers()
            }
        }
    }
    
    func getBeers() async {
        do {
            page += 1
            let url = URL(string: "https://api.punkapi.com/v2/beers?page=\(page)&per_page=30")!
            let (data, _) = try await URLSession.shared.data(from: url)
            let beersLoaded = try JSONDecoder().decode([Beer].self, from: data)
            beers = beersLoaded + beers
        } catch {
            print("Some error")
        }
    }
}

Where, for the beer, we retrieve only the name and the image:

struct Beer: Codable, Identifiable {
    var id: Int
    var name: String
    var image_url: String
}

This is the starting point. Now, suppose we have an iPad and we want to show more information:

struct Beer: Codable, Identifiable {
    var id: Int
    var name: String
    var first_brewed: String
    var image_url: String
    var abv: Double
}

To have this:

he first step is to replace the List with a TableView:

Table(beers) {
            TableColumn("Name", value: \.name)
            TableColumn("First Brew", value: \.first_brewed)
            TableColumn("ABV") { beer in
                Text("\(beer.abv)")
            }
            TableColumn("Image") { beer in
                AsyncImage(url: URL(string: beer.image_url)) { image in
                    image.resizable().scaledToFit()
                } placeholder: {
                    ProgressView()
                }
                .frame(width: 100, height: 100)
            }
        }

The table operates on the array of beers. After that, we add the column, where we assign the title name and the value (at least for the strings). For the ABV, which is a double, we pass the beer to the content (in this case, simple text), the same goes for the column that displays the image.

Now, we want to add a sorting option, to sort by name:

@State private var sortOrder = [KeyPathComparator(\Beer.name, order: .reverse)]

Change the Table definition in this way:

Table(beers, sortOrder: $sortOrder)

It’s not sufficient if we want the order to change by tapping on the column header; we have to add this:

.onChange(of: sortOrder) {
            beers.sort(using: sortOrder)

To have the beers list ordered right after it’s downloaded, we add this to the getBeers function:

beers.sort(using: sortOrder)

Merging everything together:

struct ContentView: View {
    @State var beers: [Beer] = []
    @State var page = 0
    @State private var sortOrder = [KeyPathComparator(\Beer.name, order: .reverse)]
    var body: some View {
        Table(beers, sortOrder: $sortOrder) {
            TableColumn("Name", value: \.name)
            TableColumn("First Brew", value: \.first_brewed)
            TableColumn("ABV") { beer in
                Text("\(beer.abv)")
            }
            TableColumn("Image") { beer in
                AsyncImage(url: URL(string: beer.image_url)) { image in
                    image.resizable().scaledToFit()
                } placeholder: {
                    ProgressView()
                }
                .frame(width: 100, height: 100)
                
            }
        }.onAppear {
            Task {
                await getBeers()
            }
        }.onChange(of: sortOrder) {
            beers.sort(using: sortOrder)
       }
    }
    func getBeers() async {
        do {
            page += 1
            let url = URL(string: "https://api.punkapi.com/v2/beers?page=\(page)&per_page=30")!
            let (data, _) = try await URLSession.shared.data(from: url)
            let beersLoaded = try JSONDecoder().decode([Beer].self, from: data)
            beers = beersLoaded + beers
            beers.sort(using: sortOrder)
        } catch {
            print("Some error")
        }
    }
}

Now we are able to create a sorted table.

Note: English is not my native language, so I apologize for any errors. I use AI solely to generate the banner of the post; the content is human-generated.