Swift embedded and pico SDK

In this post we’ll learn how to:

  • Install Swift embedded
  • Install the pico-sdk
  • Run a simple blink

To install Swift embedded we need to download a nightly toolchain from here:

https://www.swift.org/download/#snapshots

We can’t use offciale release because the embedded feature is not ready for production.

After that we can clone the https://github.com/apple/swift-embedded-examples.git that contains the pico-blink-sdk

This example use the c library from the swift code to blink the led.

@main
struct Main {
  static func main() {
    let led = UInt32(PICO_DEFAULT_LED_PIN)
    gpio_init(led)
    gpio_set_dir(led, /*out*/true)
    while true {
      gpio_put(led, true)
      sleep_ms(250)
      gpio_put(led, false)
      sleep_ms(250)
    }
  }
}

Introduced in Swift 5.3, the @main attribute designates a particular type as the entry point for program execution.

To compile (how is written the the github page):

$ cd pico-blink-sdk
$ export TOOLCHAINS='<toolchain-name>'
$ export PICO_BOARD=pico
$ export PICO_SDK_PATH='<path-to-your-pico-sdk>'
$ export PICO_TOOLCHAIN_PATH='<path-to-the-arm-toolchain>'
$ cmake -B build -G Ninja .
$ cmake --build build

for the PICO_TOOLCHAIN_PATH i use:

export TOOLCHAINS=$(plutil -extract CFBundleIdentifier raw /Library/Developer/Toolchains/swift-DEVELOPMENT-SNAPSHOT-2024-06-13-a.xctoolchain/Info.plist)

For the PICO_SDK_PATH you can see the previous post. About the PICO_TOOLCHAIN_PATH you could skip.

After the build, you should have your first swift embedded program: swift.blinky.uf2.

Share SwiftData with a Widget

In this post, we’ll see how to share data from the application to the widget.

First, create the project and call it SharedWidget, selecting SwiftData as storage. After that, create the widget by selecting Target in the File -> New menu and then WidgetExtension.

After the creation process, you can also deselect the Live and Intent options because we won’t be using them.

To share data between the application and the widget, we need to create an App Group in the capabilities. After that, add a group and call it whatever you want. In my case, I named it:

Then, select the widget extension in the target:

Thus, also for it, add the app group in the capabilities, but this time don’t add a new one; select the existing one.

One last thing before we look at the code. We have to share the model between the app and the widget, so select the Item.swift file and select both project components:

If you run the application at this moment, you can add an item (the default Apple example for SwiftData) and if you add the widget, you’ll see a time and an emoji. We want to simply display the number of items added below the emoji, so the first thing to do is:

struct SimpleEntry: TimelineEntry {
    let date: Date
    let emoji: String
    let number: Int
}

Where the TimeLineEntry is: “A type that specifies the date to display a widget, and, optionally, indicates the current relevance of the widget’s content.”

Now, in the provider, we add the structure to retrieve the item numbers:

@MainActor
    private func getNumberOfItems() -> Int {
        guard let modelContainer = try? ModelContainer(for: Item.self) else {
            return 0
        }
        let descriptor = FetchDescriptor<Item>()
        let number = try? modelContainer.mainContext.fetch(descriptor).count
        return number ?? 0
    }

Note the @MainActor, now we change the Provider in this way:

struct Provider: TimelineProvider {
    
    @MainActor
    func placeholder(in context: Context) -> SimpleEntry {
        SimpleEntry(date: Date(), emoji: "😀", number: getNumberOfItems())
    }

    @MainActor
    func getSnapshot(in context: Context, completion: @escaping (SimpleEntry) -> ()) {
        let entry = SimpleEntry(date: Date(), emoji: "😀", number: getNumberOfItems())
        completion(entry)
    }

    @MainActor
    func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
        print(Date())
        let timeline = Timeline(entries: [SimpleEntry(date: Date(), emoji: "😀", number: getNumberOfItems())], policy: .after(.now.advanced(by: 60)))
        completion(timeline)
    }
    
    @MainActor
    private func getNumberOfItems() -> Int {
        guard let modelContainer = try? ModelContainer(for: Item.self) else {
            return 0
        }
        let descriptor = FetchDescriptor<Item>()
        let number = try? modelContainer.mainContext.fetch(descriptor).count
        return number ?? 0
    }
}

So, every function that uses the getNumberOfItems must use the @MainActor annotation (Its role is to ensure that all code is executed on the main thread).

For more information about timelines, I advise reading the official documentation: Apple Developer – Timeline.

Now do a little changes in the view:

struct ForShareWidgetEntryView : View {
    var entry: Provider.Entry
    
    var body: some View {
        VStack {
            Text("Time:")
            Text(entry.date, style: .time)

            Text("Emoji:")
            Text(entry.emoji)
            Text("\(entry.number)")
        }
    }
}

Now everything should work. Keep in mind that in a production environment, the operating system decides when to update the widget.

Chart in SwiftUI

Charts are not present in every mobile app, but they are quite common in sports, health, and finance applications. In this post, we’ll learn how to:

  • Create a chart (bar and line)
  • Set the unit
  • Improve the visibility of the information on the bars

Create a chart

First create the structure of the data:

struct MoneyData: Identifiable {
    let id = UUID()
    let operation: Int
    let euro: Double
}

Then, add a chart to the View (note the import Charts):

import SwiftUI
import Charts

struct ContentView: View {
    @State var expences: [MoneyData] = [MoneyData]()
    var body: some View {
        VStack {
            Chart(expences, id: \.id) { ex in
                BarMark(
                    x: .value("Operation", ex.operation),
                    y: .value("Money", ex.euro)
                )
            }
            .frame(height: 300)
            .padding()
            .onAppear {
                self.expences = initMoney()
            }
        }
    }
    func initMoney() -> [MoneyData] {
        return [MoneyData(operation: 1, euro: 2.5),
                MoneyData(operation: 2, euro: 6.5), MoneyData(operation: 3, euro: 1.5)
        ]
    }
}

To have:

Next, the chart iterates over the expenses array, displaying the operation on the X-axis and the euros on the Y-axis.

To create a linear chart, use LineMark instead of BarMark:

LineMark(
         x: .value("Operation", ex.operation),
         y: .value("Money", ex.euro)
)

To have:

Note that the last operation has three values but is not displayed. To avoid this behavior, we need to add one value at the beginning and one at the end of the X-axis. Let’s see how to do this:

struct ContentView: View {
    @State var expences: [MoneyData] = [MoneyData]()
    @State var gridValue = [Int]()
    
    var body: some View {
        VStack {
            Chart(expences, id: \.id) { ex in
                LineMark(
                    x: .value("Operation", ex.operation),
                    y: .value("Money", ex.euro)
                )
            }.chartXAxis {
                AxisMarks(values: gridValue)  
            }
            .frame(height: 300)
            .padding()
            .onAppear {
                self.expences = initMoney()
                self.gridValue = Array((expences[0].operation - 1)...(expences[expences.count - 1].operation + 1))
            }
        }
    }
    func initMoney() -> [MoneyData] {
        return [MoneyData(operation: 1, euro: 2.5),
                MoneyData(operation: 2, euro: 6.5), MoneyData(operation: 3, euro: 1.5)
                
        ]
    }
}

First, we add a State variable gridValue, initialized in the onAppear method with an interval for the operation that is larger than the interval in the money data (e.g., [0,3] instead of [1,2]).

This interval is used by applying:

.chartXAxis {
                AxisMarks(values: gridValue)  
            }

Thus, we have:

Unit

Now, let’s see what happens if we use dates on the X-axis.

To use dates, first, adjust the data structure to include dates.

struct RunData: Identifiable {
    let id = UUID()
    let date: Date
    let km: Double
}

Then make some small changes in the code to use this type of data with the chart:

struct ContentView: View {
    @State var races: [RunData] = [RunData]()
    
    var body: some View {
        VStack {
            Chart(races, id: \.id) { run in
                BarMark(
                    x: .value("Date", run.date),
                    y: .value("Km", run.km)
                )
            }
            .frame(height: 300)
            .padding()
            .onAppear {
                self.races = initData()
            }
        }
    }
    func initData() -> [RunData] {
        let dateFormatter = ISO8601DateFormatter()
        dateFormatter.formatOptions = [.withFullDate]
        
        return [RunData(date: dateFormatter.date(from: "2024-05-03") ?? Date.now, km: 15),
                RunData(date: dateFormatter.date(from: "2024-05-05") ?? Date.now, km: 20),
                RunData(date: dateFormatter.date(from: "2024-05-07") ?? Date.now, km: 10)
        ]
    }
}

Simply, we replaced the moneyData with runData and used a different initialization function, just to have:

As you can see, we only have the label for “5 May.” Don’t worry, we have a simpler solution that I showed before. We can add the unit value for the X-axis:

BarMark(
         x: .value("Date", run.date, unit: .day),
         y: .value("Km", run.km)
)

Now we see:

Now you should have the knowledge base to implement charts in your app.

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.

Alert in SwiftUI

In Xcode 15, we can display an alert using the .alert modifier.

Take a look at the code:

struct ContentView: View {
    @State var showAlert = false
    
    var body: some View {
        VStack {
            Button("Show Alert") {
                showAlert = true
            }
        }.alert("Important message", isPresented: $showAlert) {
            Button("OK", role: .cancel) {
                // Your action
            }
        }
        .padding()
    }
}

The role of the button can be:

  • .cancel
  • .destructive
struct ContentView: View {
    @State var showAlert = false
    
    var body: some View {
        VStack {
            Button("Show Alert") {
                showAlert = true
            }
        }.alert("Important message", isPresented: $showAlert) {
            Button("Delete", role: .destructive) {
                
            }
        }
        .padding()
    }
}

Note that using a button with .destructive is also added the default cancel button.

LazyGrid in SwiftUI

In this post, we’ll learn how to use LazyVGrid and LazyHGrid, two powerful and efficient grids to display multiple items.

The fundamental element for these types of grids is the GridItem, which can be of three types:

  • .adaptive
  • .fixed
  • .flexible

GridItem Adaptive

GridItem Adaptive is essential for a responsive layout. By setting a minimum size for it, the grid will try to adapt to all the available space.

Take a look at using LazyHGrid:

struct ContentView: View {
    let rainbowColors = [Color.red, Color.orange, Color.yellow, Color.green, Color.mint, Color.teal, Color.cyan, Color.blue, Color.indigo, Color.purple, Color.brown]
    var body: some View {
        ScrollView(.horizontal) {
            LazyHGrid(rows: [GridItem(.adaptive(minimum:150))]) {
                        ForEach(rainbowColors, id:\.self) { color in
                            Circle()
                            .fill(color)
                        }
                    }
                    .padding()
                }
    }
}

We can also define different sizes for the rows:

ScrollView(.horizontal) {
            LazyHGrid(rows: [GridItem(.adaptive(minimum:150)),GridItem(.adaptive(minimum:100))]) {
                        ForEach(rainbowColors, id:\.self) { color in
                            Circle()
                                                    .fill(color)
                        }
                    }
                    .padding()
                }

So we have:

So, the two different dimensions are used to adapt to the screen.

This type is helpful when we have a dynamic number of elements, such as in a playlist.

Using the Vertical grid:

ScrollView() {
            LazyVGrid(columns: [GridItem(.adaptive(minimum:100)), GridItem(.adaptive(minimum:50)),GridItem(.adaptive(minimum:100))]) {
                        ForEach(rainbowColors, id:\.self) { color in
                            Circle()
                            .fill(color)
                        }
                    }
                    .padding()
                }
}

GridItem Fixed

In this case, the GridItem has a fixed size for the horizontal layout:

ScrollView(.horizontal) {
            LazyHGrid(rows: [GridItem(.fixed(100))]) {
                ForEach(rainbowColors, id:\.self) { color in
                    Circle()
                                            .fill(color)
                }
            }
            .padding()
        }

For vertical:

ScrollView() {
            LazyVGrid(columns: [GridItem(.fixed(100))]) {
                    ForEach(rainbowColors, id:\.self) { color in
                        Circle()
                        .fill(color)
                    }
                }
                .padding()
            }
}

This is helpful when you need a fixed number of columns or rows. In this case, by adding GridItem with a fixed size, we don’t have adaptability to the screen.

Take a look at this example:

ScrollView() {
            LazyVGrid(columns: [GridItem(.fixed(150)),GridItem(.fixed(150)),GridItem(.fixed(150))]) {
                    ForEach(rainbowColors, id:\.self) { color in
                        Circle()
                        .fill(color)
                    }
                }
                .padding()
            }

we have:

Three columns of circles with fixed size but truncated.

GridItem Flexible

With flexible, we define the size of GridItem within a range. For example:

ScrollView(.horizontal) {
            LazyHGrid(rows: [GridItem(.flexible(minimum: 10, maximum: 50)),GridItem(.flexible(minimum: 20, maximum: 100))]) {
                ForEach(rainbowColors, id:\.self) { color in
                    Circle()
                    .fill(color)
                }
            }
            .padding()
        }

To have:

It’s helpful when we need different sizes for different content, such as images and text.

Let’s try the vertical case for exercise.

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.