Etiqueta: Swift 6 migration

  • Writing a Barcode Reader App in No Time

    Writing a Barcode Reader App in No Time

    There are other ways to input data into your application besides using a device’s keyboard. One such method is reading barcodes. In this post, I’ll demonstrate how easy it is to implement a solution for this functionality.

    AVCaptureMetadataOutput

    AVCaptureMetadataOutput is the class responsible for intercepting metadata objects from the video stream captured during a session. Part of the AVFoundation framework, its primary purpose is to detect and process metadata in real-time while capturing video.

    Key Characteristics of AVCaptureMetadataOutput:
    1. Code Detection:
      This class can detect various types of codes, such as QR codes and barcodes, including formats like EAN-8, EAN-13, UPC-E, Code39, and Code128, among others.

    2. Flexible Configuration:
      You can specify the types of metadata you want to capture using the metadataObjectTypes property. This provides granular control over the kind of information the system processes.

    3. Delegate-Based Processing:
      Metadata detection and processing are managed via a delegate object. This approach provides flexibility in handling the detected data and enables custom responses. However, note that working with this delegate often requires integration with the UIKit framework for user interface handling.

    4. Integration with AVCaptureSession:
      The AVCaptureMetadataOutput instance is added as an output to an AVCaptureSession. This setup enables real-time processing of video data as it is captured.

    Creating iOS App sample app

    Create a new blank iOS SwiftUI APP, and do not forget set Strict Concurrency Checking to Complete and Swift Language Versionto Swift 6

    As I mention on point 3 from past section, the pattern that implements AVCaptureMetadataOutput is deletage patterns, but we want our app that uses the latest and coolest SwiftUI framework. For fixing that we will need support of our old friend UIKit. Basically wrap UIKit ViewController into a UIViewControllerRespresentable, for being accessible from SwiftUI. And finally implement delegate inside UIViewControllerRespresentable.

    Create a new file called ScannerPreview and start writing following code:

    import SwiftUI
    import AVFoundation
    
    // 1
    struct ScannerPreview: UIViewControllerRepresentable {
        @Binding var isScanning: Bool
        var didFindBarcode: (String) -> Void = { _ in }
        // 2
        func makeCoordinator() -> Coordinator {
            return Coordinator(parent: self)
        }
        // 3
        func makeUIViewController(context: Context) -> UIViewController {
            let viewController = UIViewController()
            let captureSession = AVCaptureSession()
    
            // Setup the camera input
            guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return viewController }
            let videoDeviceInput: AVCaptureDeviceInput
    
            do {
                videoDeviceInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
            } catch {
                return viewController
            }
    
            if (captureSession.canAddInput(videoDeviceInput)) {
                captureSession.addInput(videoDeviceInput)
            } else {
                return viewController
            }
    
            // Setup the metadata output
            let metadataOutput = AVCaptureMetadataOutput()
    
            if (captureSession.canAddOutput(metadataOutput)) {
                captureSession.addOutput(metadataOutput)
    
                metadataOutput.setMetadataObjectsDelegate(context.coordinator, queue: DispatchQueue.main)
                metadataOutput.metadataObjectTypes = [.ean13, .ean8, .pdf417, .upce, .qr, .aztec] // Add other types if needed
            } else {
                return viewController
            }
    
            // Setup preview layer
            let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
            previewLayer.frame = viewController.view.layer.bounds
            previewLayer.videoGravity = .resizeAspectFill
            viewController.view.layer.addSublayer(previewLayer)
    
            captureSession.startRunning()
    
            return viewController
        }
    
        func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
            // Here we can update the UI if needed (for example, stopping the session)
        }
    }

    To integrate a UIViewController into a SwiftUI View, import SwiftUI (for access to UIViewControllerRepresentable) and AVFoundation (for AVCaptureMetadataOutputObjectsDelegate).

    Key Features and Implementation
      1. UIViewControllerRepresentable Protocol
        Implementing the UIViewControllerRepresentable protocol allows a UIKit UIViewController to be reused within SwiftUI.

        • isScanning: This is a binding to the parent view, controlling the scanning state.
        • didFindBarcode: A callback function that is executed whenever a barcode is successfully scanned and read.
      2. Coordinator and Bridging

        • makeCoordinator: This method is required to fulfill the UIViewControllerRepresentable protocol. It creates a «bridge» (e.g., a broker, intermediary, or proxy) between the UIKit UIViewController and the SwiftUI environment. In this implementation, the Coordinator class conforms to the AVCaptureMetadataOutputObjectsDelegate protocol, which handles metadata detection and processing.
      3. Creating the UIViewController

        • makeUIViewController: Another required method in the protocol, responsible for returning a configured UIViewController.
          • Inside this method, the AVCaptureSession is set up to detect specific barcode formats (e.g., EAN-13, EAN-8, PDF417, etc.).
          • The configured session is added as a layer to the UIViewController.view.
        func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
            // Here we can update the UI if needed (for example, stopping the session)
        }
        
        //1
        @MainActor
        class Coordinator: NSObject, @preconcurrency AVCaptureMetadataOutputObjectsDelegate {
            var parent: ScannerPreview
            
            init(parent: ScannerPreview) {
                self.parent = parent
            }
            // 2
            // MARK :- AVCaptureMetadataOutputObjectsDelegate
            func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
                // 4
                if let metadataObject = metadataObjects.first {
                    guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
                    guard let stringValue = readableObject.stringValue else { return }
                    AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
                    self.parent.isScanning = false
                    // 3
                    parent.didFindBarcode(String(stringValue))
                }
            }
        }

    Later, we will implement the Coordinator class, which must inherit from NSObject because it needs to conform to the AVCaptureMetadataOutputObjectsDelegate protocol, an extension of NSObjectProtocol.

    Key Features and Implementation:
    1. Swift 6 Compliance and Data Race Avoidance
      To ensure compliance with Swift 6 and avoid data races, the class is executed on @MainActor. This is necessary because it interacts with attributes from its parent, UIViewControllerRepresentable. Since AVCaptureMetadataOutput operates in a non-isolated domain, we’ve marked the class with @MainActor.

    2. Thread Safety
      Before marking AVCaptureMetadataOutputObjectsDelegate with @preconcurrency, ensure the following:

      • The metadataOutput.setMetadataObjectsDelegate(context.coordinator, queue: DispatchQueue.main) call is executed on the main thread (@MainActor).
      • This guarantees that when setting up AVCaptureMetadataOutput, it operates safely on the main thread.
    3. Data Handling
      The parent view receives a copy of the scanned barcode string. At no point does the delegate implementation modify the received data. This ensures thread safety and avoids potential data races.

    4. Protocol Method Implementation
      In the protocol method implementation:

      • Fetch the first object.
      • Retrieve the barcode value.
      • Update the scanning state.
      • Execute the callback function.

    By ensuring that no data is modified across different isolated domains, it is safe to proceed with marking the protocol with @preconcurrency.

     

    Final step is just implent the SwiftUI view where ScannerPreview view will be embeded. Create a new file called BarcodeScannerView and write following code:

    import SwiftUI
    import AVFoundation
    
    struct BarcodeScannerView: View {
        @State private var scannedCode: String?
        @State private var isScanning = true
        @State private var showAlert = false
        
        var body: some View {
            VStack {
                Text("Scan a Barcode")
                    .font(.largeTitle)
                    .padding()
    
                ZStack {
                    //1
                    ScannerPreview(isScanning: $isScanning,
                                   didFindBarcode: { value in
                        scannedCode = value
                        showAlert = true
                    }).edgesIgnoringSafeArea(.all)
    
                    VStack {
                        Spacer()
                        HStack {
                            Spacer()
                            if let scannedCode = scannedCode {
                                Text("Scanned Code: \(scannedCode)")
                                    .font(.title)
                                    .foregroundColor(.white)
                                    .padding()
                            }
                            Spacer()
                        }
                        Spacer()
                    }
                }
    
                if !isScanning {
                    Button("Start Scanning Again") {
                        self.isScanning = true
                        self.scannedCode = nil
                    }
                    .padding()
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(8)
                }
            }
            .onAppear {
                self.scannedCode = nil
                self.isScanning = true
            }
        }
    }
    Key Features and Implementation:
    1. Just place the preview in a ZStack and implment the callback to execute when the barcode is read.

    import SwiftUI
    
    struct ContentView: View {
        var body: some View {
            BarcodeScannerView()
        }
    }
    
    #Preview {
        ContentView()
    }
    

    Last but not least be sure that ContentView is executing the view that we have just created. And be sure that you have a description for NSCameraUsageDescription setting.

    Build and Run on real device

    For executing the app be sure that you deploy on a real device (iPhone or iPad). Whem the app ask you permission for using the camera, obviously say allow.

    Conclusions

    In this post, you have seen how easy it is to implement a barcode scanner using native libraries. You can find the working code used in this post in the following repository.

    References

  • Firebase Authentication in Your iOS App

    Firebase Authentication in Your iOS App

    User authentication is often a cumbersome task that becomes essential as an application grows more robust. Firebase Authentication simplifies this process by handling authentication for you. It supports several authentication methods, but for the purpose of this post, we will focus on email and password authentication.

    Firebase console

    The first step is to create a dashboard to manage your app’s Firebase capabilities. To do this, open the Firebase console and create a new project. Use the name of your app as the project name for better organization and clarity. For the purposes of this post, I will not enable analytics. With these steps completed, your project should be ready to use.

    Later on, we will revisit the setup-specific issues for your iOS app.

    Creating iOS App scaffolder

    Let’s set Firebase aside for a moment and create a blank application. The only important detail in this process is to pay attention to the Bundle Identifier, as we’ll need it in the upcoming steps.

    Connect Firebase to your app

    Return to Firebase to add it to your app, and select the iOS option

    This is the moment to enter your app’s iOS Bundle Identifier.

    The next step is to download the configuration file and incorporate it into your project.

    The final step is incorporating the Firebase SDK using Swift Package Manager (SPM). To do this, select your project, go to Package Dependencies, and click Add Package Dependency.

    Enter the GitHub repository URL provided in Step 3 of the Firebase configuration into the search input box.

    Don’t forget to add FirebaseAuth to your target application.

    Continue through the Firebase configuration steps, and it will eventually provide the code you need to connect your app to Firebase.

    Incorporate this code into your iOS app project, then build and run the project to ensure everything is working properly.

    Implement authentication

    Get back to Firebase console to set up Authentication

    As you can see, there are many authentication methods, but for the purpose of this post, we will only work with the email and password method.

    As you can see, there are many authentication methods, but for the purpose of this post, we will only focus on the email and password method.

    For this post, I have implemented basic views for user registration, login, logout, and password recovery. All authentication functionality has been wrapped into a manager called AuthenticatorManager. The code is very basic but fully functional and compliant with Swift 6.0.

    Don’t worry if I go too fast; the link to the code repository is at the end of this post. You’ll see that the code is very easy to read. Simply run the project, register an account, and log in.

    You can find the project code at following repository.

    Conclusions

    Firebase provides a fast and efficient way to handle user authentication as your app scales.

  • Swift 6 migration recipes

    Swift 6 migration recipes

    Swift’s concurrency system, introduced in Swift 5.5, simplifies the writing and understanding of asynchronous and parallel code. In Swift 6, language updates further enhance this system by enabling the compiler to ensure that concurrent programs are free of data races. With this update, compiler safety checks, which were previously optional, are now mandatory, providing safer concurrent code by default.

    Sooner or later, this will be something every iOS developer will need to adopt in their projects. The migration process can be carried out incrementally and iteratively. The aim of this post is to present concrete solutions for addressing specific issues encountered while migrating code to Swift 6. Keep in mind that there are no silver bullets in programming.

    Step 0. Plan your strategy

    First, plan your strategy, and be prepared to roll back and re-plan as needed. That was my process, after two rollbacks 🤷:

    1. Set the «Strict Concurrency Checking» compiler flag on your target. This will bring up a myriad of warnings, giving you the chance to tidy up your project by removing or resolving as many warnings as possible before proceeding.
    2. Study Migration to Swift 6 (from swift.org), Don’t just skim through it; study it thoroughly. I had to roll back after missing details here. 
    3. Set the «Strict Concurrency Checking» compiler flag to Complete. This will trigger another round of warnings. Initially, focus on moving all necessary elements to @MainActor to reduce warnings. We’ll work on reducing the main-thread load later.
      1. Expect @MainActor propagation. As you apply @MainActor, it’s likely to propagate. Ensure you also mark the callee functions as @MainActor where needed
      2. In protocol delegate implementations, verify that code runs safely on @MainActor. In some cases, you may need to make a copy of parameters to prevent data races.
      3. Repeat the process until you’ve resolved all concurrency warnings.
      4. Check your unit tests, as they’ll likely be affected. If all is clear, change targets and repeat the process..
    4. Set the Swift Language Version to Swift 6 and run the app on a real device to ensure it doesn’t crash. I encountered a crash at this stage..
    5. Reduce @MainActor usage where feasible. Analyze your code to identify parts that could run in isolated domains instead. Singletons and API services are good candidates for offloading from the main thread.

     

    On following sections I will explain the issues that I had found and how I fix them.

    Issues with static content

    Static means that the property is shared across all instances of that type, allowing it to be accessed concurrently from different isolated domains. However, this can lead to the following issue:

    Static property ‘shared’ is not concurrency-safe because non-‘Sendable’ type ‘AppGroupStore’ may have shared mutable state; this is an error in the Swift 6 language mode

    The First-Fast-Fix approach here is to move all classes to @MainActor.

    @MainActor
    final class AppGroupStore {
        let defaults = UserDefaults(suiteName: "group.jca.EMOM-timers")
        static let shared = AppGroupStore()
        private init() {
        }
    }

    Considerations:

    Moving to @MainActor forces all calls to also belong to @MainActor, leading to a propagation effect throughout the codebase. Overusing @MainActor may be unnecessary, so in the future, we might consider transitioning to an actor instead.

    For now, we will proceed with this solution. Once the remaining warnings are resolved, we will revisit and optimize this approach if needed.

     

    Issues with protocol implementation in the system or third-party SDK.

    First step: focus on understanding not the origin, but how this data is being transported. How is the external dependency handling concurrency? On which thread is the data being dellivered? As a first step, check the library documentation — if you’re lucky, it will have this information. For instance:

    • CoreLocation: Review the delegate specification in the CLLocationManager documentation. At the end of the overview, it specifies that callbacks occur on the same thread where you initialized the CLLocationManager.
    • HealthKit: Consult the HKWorkoutSessionDelegate documentation. Here, the overview mentions that HealthKit calls these methods on an anonymous serial background queue.
     

    In my case, I was working with WatchKit and implementing the WCSessionDelegate. The documentation states that methods in this protocol are called on a background thread.

    Once I understand how the producer delivers data, I need to determine the isolated domain where this data will be consumed. In my case, it was the @MainActor due to recursive propagation from @MainActor.

    Now, reviewing the code, we encounter the following warning:

    Main actor-isolated instance method ‘sessionDidBecomeInactive’ cannot be used to satisfy nonisolated protocol requirement; this is an error in the Swift 6 language mode.

    In case this method had no implementation just mark it as nonisolated and move to next:

    nonisolated func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
     }

    The next delegate method does have an implementation and runs on the @MainActor, while the data is delivered on a background queue, which is a different isolated domain. I was not entirely sure whether the SDK would modify this data.

    My adaptation at that point was create a deep copy of data received, and forward the copy to be consumed..

    nonisolated func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
         doCopyAndCallUpdateInMainActor(userInfo)
    }
       
    private nonisolated func doCopyAndCallUpdateInMainActor(_ dictionary: [String: Any] = [:])  {
        nonisolated(unsafe) let dictionaryCopy =  dictionary.deepCopy()
            Task { @MainActor in
                await self.update(from: dictionaryCopy)
            }
    }

    Issues with default parameter function values

    I have a function that receives a singleton as a default parameter. The singletons are working in an isolated domain; in this case, it was under @MainActor. I encountered the following issue:

    Main actor-isolated static property ‘shared’ can not be referenced from a nonisolated context; this is an error in the Swift 6 language mode

    To remove this warning, I made it an optional parameter and handled its initialization:

    init(audioManager: AudioManagerProtocol? = nil,
             extendedRuntimeSessionDelegate: WKExtendedRuntimeSessionDelegate? = nil) {
            self.audioManager = audioManager ?? AudioManager.shared
            self.extendedRuntimeSessionDelegate = extendedRuntimeSessionDelegate
        }

    Decoupling non-UI logic from @MainActor for better performance.

    There are components in your application, such as singletons or APIs, that are isolated or represent the final step in an execution flow managed by the app. These components are prime candidates for being converted into actors.

    Actors provide developers with a means to define an isolation domain and offer methods that operate within this domain. All stored properties of an actor are isolated to the enclosing actor instance, ensuring thread safety and proper synchronization.

    Previously, to expedite development, we often annotated everything with @MainActor.

    @MainActor
    final class AppGroupStore {
        let defaults = UserDefaults(suiteName: "group.jca.XYZ")
        
        static let shared = AppGroupStore()
        
        private init() {
            
        }
    }

    Alright, let’s move on to an actor.

    actor AppGroupStore {
        let defaults = UserDefaults(suiteName: "group.jca.XYZ")
        
        static let shared = AppGroupStore()
        
        private init() {
            
        }
    }

    Compiler complains:

    Actor ‘AppGroupStore’ cannot conform to global actor isolated protocol ‘AppGroupStoreProtocol’

    That was because protocol definition was also @MainActor, so lets remove it:

    import Foundation
    //@MainActor
    protocol AppGroupStoreProtocol {
        func getDate(forKey: AppGroupStoreKey) -> Date?
        func setDate(date: Date, forKey: AppGroupStoreKey)
    }

    At this point, the compiler raises errors for two functions: one does not return anything, while the other does. Therefore, the approaches to fixing them will differ.

    We have to refactor them to async/await

    protocol AppGroupStoreProtocol {
        func getDate(forKey: AppGroupStoreKey) async -> Date?
        func setDate(date: Date, forKey: AppGroupStoreKey) async
    }

    There are now issues in the locations where these methods are called.

    This function call does not return any values, so enclosing its execution within Task { ... } is sufficient.

        func setBirthDate(date: Date) {
            Task {
                await AppGroupStore.shared.setDate(date: date, forKey: .birthDate)
            }
        }

    Next isssue is calling a function that in this case is returning a value, so the solution will be different.

    Next, the issue is calling a function that, in this case, returns a value, so the solution will be different.

        func getBirthDate() async -> Date {
            guard let date = await AppGroupStore.shared.getDate(forKey: .birthDate) else {
                return Calendar.current.date(byAdding: .year, value: -25, to: Date()) ?? Date.now
            }
            return date
        }

    Changing the function signature means that this change will propagate throughout the code, and we will also have to adjust its callers.

    Just encapsulate the call within a Task{...}, and we are done.

            .onAppear() {
                Task {
                    guard await AppGroupStore.shared.getDate(forKey: .birthDate) == nil else { return }
                    isPresentedSettings.toggle()
                }
            }

    Conclusions

    To avoid a traumatic migration, I recommend that you first sharpen your saw. I mean, watch the WWDC 2024 videos and study the documentation thoroughly—don’t read it diagonally. Once you have a clear understanding of the concepts, start hands-on work on your project. Begin by migrating the easiest issues, and as you feel more comfortable, move on to the more complicated ones.

    At the moment, it’s not mandatory to migrate everything at once, so you can start progressively. Once you finish, review if some components could be isolated into actors.

    Related links