Code Reviews - forceUnwrap https://forceunwrap.com Lift your Swift skills & career to the next level Thu, 09 Mar 2023 18:48:37 +0000 en-US hourly 1 https://wordpress.org/?v=7.0 https://forceunwrap.com/wp-content/uploads/2023/03/cropped-ForceUnwrap-32x32.jpg Code Reviews - forceUnwrap https://forceunwrap.com 32 32 Multiple sections from single data source SwiftUI https://forceunwrap.com/multiple-sections-from-single-data-source-swiftui/ https://forceunwrap.com/multiple-sections-from-single-data-source-swiftui/#respond Tue, 09 Aug 2022 12:59:37 +0000 https://forceunwrap.com/?p=222
The specification’s are rather unclear. After reading everything more than once, I came to conclusion, that the desired outcome is to have a single source of truth, with different views for the same data.

We loop through categories, to create sections. Later on we loop through indices to actually display the content from Message struct.

import SwiftUI

struct MultipleSectionsView: View {
    @State private var messages = [
        Message(content: "Hello", category: .important, isSelected: false),
        Message(content: "World", category: .significant, isSelected: false),
        Message(content: "Hello", category: .important, isSelected: false),
        Message(content: "World", category: .significant, isSelected: false),
        Message(content: "Hello", category: .important, isSelected: false),
        Message(content: "World", category: .pointless, isSelected: false)
    ]

    var body: some View {
        Form {
            ForEach(Message.MessageCategory.allCases, id: \.self) { category in
                Section(category.rawValue.uppercased()) {
                    ForEach(messages.indices,  id:\.self) { index in
                        if messages[index].category == category {
                            HStack {
                                Image(
                                    systemName:
                                        messages[index].isSelected ? "checkmark.circle" : "circle"
                                )
                                
                                Text(messages[index].content)
                            }
                            .onTapGesture {
                                messages[index].toggleSelection()
                            }
                        }
                    }
                }
            }
        }
    }
}

struct Message: Identifiable {
    let id = UUID()
    let content: String
    let category: MessageCategory

    var isSelected: Bool

    mutating func toggleSelection() {
        isSelected = !isSelected
    }

    enum MessageCategory: String, CaseIterable {
        case pointless, important, significant
    }
}

In ForEach we need to use id. By using id we observe changes to the indices as well. Otherwise we could possibly get a crash by running out of bounds. You can test this out by simply adding messages = messages.dropLast() to the .onTapGesture closure and removing id:.self in the ForEach.

Not the most efficient solution, but I guess it does the job.

Homework: refactor code to a more efficient approach. At the moment we are looping through whole messages for each category. The solution should allow getting the data, without looping through messages for each category. Any other ideas for improvement?

The post Multiple sections from single data source SwiftUI first appeared on forceUnwrap.

]]>
https://forceunwrap.com/multiple-sections-from-single-data-source-swiftui/feed/ 0
UserDefaults in SwiftUI Environment with AppStorage https://forceunwrap.com/userdefaults-in-swiftui-environment/ https://forceunwrap.com/userdefaults-in-swiftui-environment/#respond Wed, 20 Jul 2022 19:13:03 +0000 https://forceunwrap.com/?p=197 Here’s an improved version of your written text:

While browsing Reddit recently, I came across a question that prompted me to start this blog. I wanted to share my expertise with others so that we can all become better developers.

As I was reviewing some code, I noticed that sensitive data such as login information should not be saved to UserDefaults. Instead, it should be stored in Keychain. However, we can use AppStorage for other types of data.

To demonstrate this, I’ve created a SampleStorage class that uses AppStorage to store and retrieve data. This class includes methods for saving and retrieving employee information in a dictionary format. Since the data is stored in a dictionary in dictionary format, we need to encode and decode it before saving to UserDefaults (line 9 and 14).

final class SampleStorage: ObservableObject {
    @AppStorage("name")
    var name: String = ""
    
    @AppStorage("employeeInformation")
    var employeeData: Data = Data()
    
    func saveEmployeeInformation(info: [[String: String]]) {
        guard let newData = try? JSONEncoder().encode(info) else { return }
        employeeData = newData
    }
    
    func employeeInformation() -> [[String: String]]? {
        guard let employeeInformation = try? JSONDecoder().decode([[String: String]].self, from: employeeData)
        else {
            return nil
        }
        return employeeInformation
        
        // Advanced: - One liner return approach -
        // return try? JSONDecoder().decode([[String: String]].self, from: employeeData)
    }
}

Here’s a sample view that demonstrates how to use the SampleStorage class to store and retrieve employee information.

import SwiftUI

struct UserDefaultSampleView: View {
    @ObservedObject
    private var storage = SampleStorage()
    
    var body: some View {
        VStack {
            Text("Name: " + storage.name)
                .padding()
            
            Text("Employee information: ")
            Text(storage.employeeInformation()?.description ?? "")
            
            Divider()
            
            updateNameButton
            updateEmployeeInformationButton
            removeEmployeeInformation
        }
    }
    
    // MARK: - Private -
    
    @ViewBuilder
    private var updateNameButton : some View {
        Button("Update name") {
            guard let name = randomName() else { return }
            storage.name = name
        }
        .padding()
    }
    
    @ViewBuilder
    private var updateEmployeeInformationButton: some View {
        Button("Update employee information") {
            guard let name = randomName(),
                  let middleName = randomName(),
                  let lastName = randomName()
            else {
                return
            }
            
            let information = [
                ["name": name],
                ["middleName": middleName],
                ["lastName": lastName]
            ]
            storage.saveEmployeeInformation(info: information)
        }
        .padding()
    }
    
    @ViewBuilder
    private var removeEmployeeInformation: some View {
        Button("Remove employee information") {
            storage.employeeData = Data()
        }
        .padding()
    }
    
    private func randomName() -> String? {
        let names = ["Zoey", "Chloe", "Amani", "Amaia", "Tom", "John"]
        guard let randomName = names.randomElement() else { return nil }
        return randomName
    }
}

Feel free to post any suggestions, questions in the comments below.

The post UserDefaults in SwiftUI Environment with AppStorage first appeared on forceUnwrap.

]]>
https://forceunwrap.com/userdefaults-in-swiftui-environment/feed/ 0