News in X14 and AnyLayout

New canvas layout and settings

In XCode 14 we have some changes that can help the developers. The main things are in the canvas (UI preview).

The button1 only changed the position, it’s to run the preview.
Clicking the button2 it’s possible to select the single element on the canvas.
With the button3, see:

So, with this context menu is possible to see the view with the different colour schema (light/dark) in the different orientations and sizes.
Clicking the button4 it’s set the property of the device in the canvas (colour schema, orientation and size).
Last but not least, with button5 the preview is executed on the real device connected to the computer.

The pin button

Often happen that the views are complex and composed of different elements. In that case, you can like see the complete view and change a property of a single component and see in real-time what happens in the complete view, now it’s possible without switching between the whole view and the single component, using the pin button.
Let’s create a new project, we want to add only a custom button defined in a different view file:

struct CustomButtonView: View {
    var body: some View {
        Button(action: {}) {
            Text("Click Me!")
                .fontWeight(.heavy)
                .padding(50)
                .foregroundColor(.white)
                .background(.red)
                .clipShape(Circle())
                
        }
    }
}

So in the ContentView we have the:

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text("Hello, world!")
            CustomButtonView()
        }
        .padding()
    }
}

Now we want to change the button’s colour but see in real-time what happens in the ContentView. Select the ContentView and click the pin button (purple box):

Now select the CustomButtonView in the files project list, so we see the code of the custom button in the preview but we see the “pinned” view, the ContentView.

We can change the colour and see what happens in the ContentView. (If you want, you can get the code from https://github.com/niqt/NewX14

AnyLayout

With the last release of SwiftUI is now possible to swap automatically from the different Layout, Horizontal and Vertical. Now we have HStackLayout and VStackLayout can be used to choose the layout following the orientation.

struct ContentView: View {
    @Environment(\.horizontalSizeClass) var horizontalSizeClass
    
    var body: some View {
        var layout = (horizontalSizeClass == .regular) ? AnyLayout(HStackLayout()) : AnyLayout(VStackLayout())
        layout {
                Image(systemName: "globe")
                    .imageScale(.large)
                    .foregroundColor(.accentColor)
                Image(systemName: "car")
                    .imageScale(.large)
                    .foregroundColor(.accentColor)
                Image(systemName: "pencil")
                    .imageScale(.large)
                    .foregroundColor(.accentColor)
            
        }
        
        
    }
}

With the horizontalSizeClass we can know the orientation of the device and select the layout that we want.

The image speaks more than one thousand words:

Thus, use VStackLayout and HStackLayout when you need to change the layout dynamically in other cases use HStack and VStack.

Note: English is not my native language, so I’m sorry for some errors. I appreciate it if you correct me.

iOS Tracking Transparency

You must use the AppTrackingTransparency framework if your app collects data about end users and shares it with other companies for purposes of tracking across apps and web sites (https://developer.apple.com/documentation/apptrackingtransparency).

In this post we’ll see how implement what iOS require for the tracking and i’ll use this example also to explain other concepts.

Let’s start from the end, we want this:

Swift with Storyboard

Create a iOS project using a storyboard:

After that, we add a WebView to the storyboard and apply the constrains to have the webview in full-screen. We must add a string value for the “Privacy – Tracking Description usage” in the info tab. This message have to explain because the user is tracked.

First add an outlet for the webview:

import UIKit
import WebKit

class ViewController: UIViewController {

    @IBOutlet weak var webView: WKWebView!
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

We have to add the import for the Tracking API in the SceneDelegate.swift:

import AdSupport
import AppTrackingTransparency

Now add the code to show the dialog.

struct RequestManager {
    static func getIDFA() -> String {
        return ASIdentifierManager.shared().advertisingIdentifier.uuidString
    }
    
    static func checkTrackingStatus(completionHandler: @escaping (ATTrackingManager.AuthorizationStatus) -> Void) {
        ATTrackingManager.requestTrackingAuthorization { status in
            DispatchQueue.main.async {
                completionHandler(status)
                
            }
        }
    }
}

We create a struct RequestManager, it has two static function, the getIDFA return the Identifier For Advertisers, an UUID used to track the device.

The function checkTrackingStatus is a static function (because is defined in a struct, see https://holyswift.app/the-difference-between-class-func-and-static-func-in-swift-and-why-polymorphism-matters for long explaination) that show (in async way) the dialog to get the permission from the user.

See what’s change in the SceneDelegate:

func sceneDidBecomeActive(_ scene: UIScene) {
        // Called when the scene has moved from an inactive state to an active state.
        // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
        attRequest()
    }

func attRequest() {
        RequestManager.checkTrackingStatus { status in
            switch status {
            case .authorized:
                print("Authorized")
            case .denied:
                print("Denied")
            case .notDetermined:
                print("NotDetermined")
                self.attRequest()
            case .restricted:
                print("Restricted")
            @unknown default:
                break
            }
        }
    }

In the sceneDidBecomeActive call the our function attRequest; this function call checkTrackingStatus with a closure, this function gets the status, if the status is notDetermined, the function call itself to re-ask the authorization. Note that we get also the IDFA but in our case we don’t use it.

The code https://github.com/niqt/ShowWebsite

In SwiftUI

How have this using SwiftUI? More simple, add always the string in the plist.info to request the permission and in the code write:

import SwiftUI
import AdSupport
import AppTrackingTransparency

struct ContentView: View {
    var body: some View {
            VStack {
                Text("Got the permission!")
            }.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) {  _ in
                ATTrackingManager.requestTrackingAuthorization(completionHandler: { status in
                    switch status {
                    case .authorized:
                        print("A")
                    case .denied:
                        print("D")
                    case .notDetermined:
                        print("E")
                    case .restricted:
                        print(" restricted")
                    @unknown default:
                        break
                    }
                    
                })
            }
        }
}

In this case the view require the tracking authorization calling the ATTrackingManager in the onReceive of the VStack (just for example). That’s all.

Note: English is not my native language, so I’m sorry for some errors. I appreciate it if you correct me.

SwiftUI and Bluetooth

In this post, I show how to connect a Bluetooth device from SwiftUI and how to receive data.

The code used in this example is here https://github.com/niqt/Blue

Proceed for step: the things to do are: check if the Bluetooth is enabled, scan to search the devices, connect with one and wait for data. The check of the Bluetooth status and the connection is a feature of the connection manager, the others of the peripheral. Let’s see it in details.

Every device has a service, identified with a UUID and any service can have one more property. The properties can be read-only (to read value) or read-write (write is like send a command). Also, the properties are identified with a UUID.

Let’s look at the code:

import Foundation
import CoreBluetooth

Then define the peripherical struct:

struct Peripheral: Identifiable {
    let id: Int
    let name: String
    let rssi: Int
}

We use this struct to expose the peripherical to SwiftUI.

Now, start to look at the core of the application:

class BLEManager: NSObject, ObservableObject, CBCentralManagerDelegate, CBPeripheralDelegate {
    var centralBE: CBCentralManager!
    @Published var isSwitchedOn = false
    @Published var peripherals = [Peripheral]()
    var peripheralsId = [UUID]()
    var thermometer: CBPeripheral!
    var serviceId = CBUUID(string: "00000001-710e-4a5b-8d75-3e5b444b3c3f")
    var readNotify = CBUUID(string: "00000002-710e-4a5b-8d75-3e5b444b3c3f")
    var readWrite = CBUUID(string: "00000003-710e-4a5b-8d75-3e5b444b3c3f")

The BLEManger is an NSObject that is observable (from the SwiftUI side) and implements the protocols to manage the Bluetooth status and services. Instead, the CBPeripheralDelegate protocol it’s about peripherical and properties operation.

So, the CBCentralManager is the Bluetooth manager. The isSwitchedOn contains the status of the Bluetooth and the peripherals array contains the list of the peripherals. These last two variables are published, so the value can be get from SwiftUI.

The thermometer peripheral contains the data of the peripheral on that we want to work in this example, the temperature of a RaspberryPI (the code is here https://github.com/Douglas6/cputemp). Thus, with our app, we want to read the CPU temp of a raspberry. The serviceId and properties are from this Raspberry example, you are free to replace them with anything you want.

With the init, simply initialize the manager and the delegate:

override init() {
        super.init()
        centralBE = CBCentralManager(delegate: self, queue: nil)
        centralBE.delegate = self
}

This function is called when the status changes:

func centralManagerDidUpdateState(_ central: CBCentralManager) {
        if central.state == .poweredOn {
            isSwitchedOn = true
        }
        else {
            isSwitchedOn = false
        }
}

If the state is .poweredOn (the Bluetooth is enabled on the device) we can start the scanning:

func startScanning() {
        print("startScanning")
        centralBE.scanForPeripherals(withServices: nil, options: nil)
}

After the scanning is started, with this function it’s possible to see the device around us:

func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
        var peripheralName: String!
        
        if let name = advertisementData[CBAdvertisementDataLocalNameKey] as? String {
            peripheralName = name
            let newPeripheral = Peripheral(id: peripherals.count, name: peripheralName, rssi: RSSI.intValue)
            if !peripheralsId.contains(peripheral.identifier) && peripheralName == "Thermometer" {
                peripheralsId.append(peripheral.identifier)
                peripherals.append(newPeripheral)
                stopScanning()
                self.thermometer = peripheral
                self.thermometer.delegate = self
                self.centralBE.connect(peripheral, options: nil)
            }
        }
        else {
            peripheralName = "Unknown"
        }
    }

If the Thermometer peripheral is discovered, it’s added the the array of peripheral, the scanning is stopped, set the delegate for the peripheral to the self and to simplify connect to device (without user interaction).

If the connection has success, this function is called:

 func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
        print("DidConnect")
        discoverServices(peripheral: peripheral)
    }

Thus starts to discover the services:

func discoverServices(peripheral: CBPeripheral) {
        peripheral.discoverServices([serviceId])
    }

If at least one service is discovered, start to search characteristics:

func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
        guard let services = peripheral.services else {
            print("ERROR didDiscoverServices")
            return
        }
        if services.count > 0 {
            discoverCharacteristics(peripheral: peripheral)
        }
    }

When the characteristic is found:

func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
        service.characteristics?.forEach { char in
            print("CHAR id: \(char.uuid.uuidString) VALUE: \(String(describing: char.value) )")
            peripheral.readValue(for: char)
            
        }
    }

In the BLEManger.swift file you can see also other two lines functions, their name are. self-explanatory.

Take a look to the SwiftUI code:

struct ContentView: View {
    @ObservedObject var bleManager = BLEManager()
    @State private var scan = false
    
    var body: some View {
        VStack (spacing: 10) {
            Text("STATUS")
                .font(.headline)
            
            if bleManager.isSwitchedOn {
                Text("ON")
                    .foregroundColor(.green)
            }
            else {
                Text("OFF")
                    .foregroundColor(.red)
            }
            HStack {
                Spacer()
                Toggle("Scan", isOn: $scan)
                    .toggleStyle(SwitchToggleStyle(tint: .red))
                    .onChange(of: scan) { value in
                        // action...
                        self.bleManager.startScanning()
                    }
                
            }
            Text("Bluetooth Devices")
                .font(.largeTitle)
                .frame(maxWidth: .infinity, alignment: .center)
            List(bleManager.peripherals) { peripheral in
                HStack {
                    Text(peripheral.name)
                    Spacer()
                    Text(String(peripheral.rssi))
                }
            }.frame(height: 300)
            Spacer()
        }
    }
}

We can define an observable object, the BLEManager, so we can get the Bluetooth status, with the toggle button active the scan and in the end, show the peripheral list in a SwiftUI list. Enjoy with your peripheral!

The result

Note: English is not my native language, so I’m sorry for some errors. I appreciate it if your correct me.

Pass by Reference and Value in Swift

A concept not simple to understand in every programming language (mainly for the “new” developer) is to realize the difference between “Pass by reference and Pass by value”.

Pass by value means: “copy a value from a location of memory to another location of the memory.” Instead, pass by reference mean that we don’t copy a value in the destination but the reference (address) to the source.

I like to use these images to explain the differences:

Pass by value
Pass by Value
Pass by reference
Pass by reference

Thus, when we pass by value is like copying a cell value from one cell to another, instead, when passing by reference, we set in the destination cell the address of the source (e5 in the example).

Let’s see with swift:

struct BoxStruct {
    var width = 0
    var height = 0
}

var littleBox = BoxStruct(width: 2, height: 3)
var bigBox = BoxStruct(width: 57, height: 84)

littleBox = bigBox

print("Little box: width \(littleBox.width) height \(littleBox.height)")
print("Bix box: width \(bigBox.width) height \(bigBox.height)")

bigBox.width = 150
bigBox.height = 333

print("Little box: width \(littleBox.width) height \(littleBox.height)")
print("Big box: width \(bigBox.width) height \(bigBox.height)")

Executing this code have:

Little box: width 57 height 84
Big box: width 57 height 84
Little box: width 57 height 84
Big box: width 150 height 333

In this case, we have a copy by value. We copy the content from bigBox to littleBox, thus both the boxes have the same value; after we change the value of bigBox and then the two boxes will contain different values.

Let’s see what happen with the class:

class TriangleClass {
    var width: Int
    var height: Int
    
    init(width: Int, height: Int) {
        self.width = width
        self.height = height
    }
}

var littleTriangle = TriangleClass(width: 2, height: 3)
var bigTriangle = TriangleClass(width: 57, height: 84)

littleTriangle = bigTriangle

print("Little Triangle: width \(littleTriangle.width) height \(littleTriangle.height)")
print("Big Triangle: width \(bigTriangle.width) height \(bigTriangle.height)")

bigTriangle.width = 150
bigTriangle.height = 333

print("Little Triangle: width \(littleTriangle.width) height \(littleTriangle.height)")
print("Big Triangle: width \(bigTriangle.width) height \(bigTriangle.height)")

Now we have this output:

Little Triangle: width 57 height 84
Big Triangle: width 57 height 84
Little Triangle: width 150 height 333
Big Triangle: width 150 height 333

This time is different! After the copy, the triangles have the same value; after we change the dimensions of the bigTriangle but happen a magic thing, the dimensions of the littleTriangle are changed! Why? For the class, the copy is done by reference, so writing “littleTrianlge = bigTriangle”, we are not copying the content of the big in the little, we are copying the reference like in the sheet example. Now every change in the big will be visible also in the little.

The same happen in the function, if we want modify the original value of the parameter passed to a function, these have be passed by reference, in this way:

func swap(a : inout Int, b: inout Int) {
    let dum = a
    a = b
    b = dum
}

var first = 2
var second = 5

swap(a:&first, b:&second)

// now first = 5 and second = 2

For exercise try to remove the keyword “inout” and see what happen.

Expand List Item

In this post, we see how to expand a list item of a list. To do this is not complicated, the principle is simple when the user taps the row, we insert the current Item in a Set. The view used to show the current item change the visualization type if the current item is in the set of selected items.

Before starting copy the images: https://nicoladefilippo.com/wp-content/uploads/2021/05/blogger-336371_1920-1536×1024.jpg and https://nicoladefilippo.com/wp-content/uploads/2021/05/desk-593327_1920-1536×1024.jpg in your assets directory, renaming them blogger and desktop (you can use also other images if you prefer).

The final behaviour is:

Define the data model for the list:

struct Post: Identifiable, Hashable {
    var id = UUID()
    var title: String
    var image: String
}

The view:

struct ExpandCardUIView: View {
    @State private var selection: Set<Post> = []
    
    var posts = [Post(title: "Post1", image: "blogger"), Post(title: "Post2", image: "desk"),
                 Post(title: "Post1", image: "blogger"), Post(title: "Post2", image: "desk"),
                 Post(title: "Post1", image: "blogger"), Post(title: "Post2", image: "desk"),
                 Post(title: "Post1", image: "blogger"), Post(title: "Post2", image: "desk"),
                 Post(title: "Post1", image: "blogger"), Post(title: "Post2", image: "desk"),
                 Post(title: "Post1", image: "blogger"), Post(title: "Post2", image: "desk"),
    ]
    var body: some View {
        NavigationView {
            List(posts) { post in
                RowView(post: post, isExpanded: self.selection.contains(post))
                .onTapGesture {
                    self.selectDeselect(post)
                }
                
            }
            .navigationBarTitle("I miei post")
        }
    }
    
    private func selectDeselect(_ post: Post) {
            if selection.contains(post) {
                selection.remove(post)
            } else {
                selection.insert(post)
            }
        }
}

Take a look at the logic, at beginning define:

@State private var selection: Set<Post>

In this Set are added the Post when the user Tap the row. If the user re-tap the row, the Post is removed from the Set. This is done by calling the function selectDeselect.

The row is shown with:

struct RowView: View {
    var post: Post
    let isExpanded: Bool
    var body: some View {
        VStack {
            HStack {
                Text(post.title)
            }
            if isExpanded {
                HStack {
                    BigRow(post:post)
                }
            }
        }
    }
    
}

The isEpanded is true when the selection (the Set) contains a post, in that case, the row shows also the BigRow, simply an image and another title in the body.

struct BigRow: View {
    var post: Post
    var body: some View {
        VStack {
            Image(post.image)
                .resizable()
                .aspectRatio(contentMode: .fit)
                
            Text(post.title)
                .fontWeight(.heavy)
                .font(.system(.headline, design: .rounded))
        }
    }
}

Note: English is not my native language, so I’m sorry for some errors. I appreciate it if your correct me.