SwiftUI - 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 SwiftUI - forceUnwrap https://forceunwrap.com 32 32 Probably the best guide to Swift Timer https://forceunwrap.com/probably-the-best-guide-to-swift-timer/ https://forceunwrap.com/probably-the-best-guide-to-swift-timer/#respond Tue, 30 Aug 2022 13:20:18 +0000 https://forceunwrap.com/?p=254 Swift’s Timer class allows us to schedule work to be repeated or run once. In SwiftUI, we can use the Timer.publish method to create a timer that runs every second and updates a state variable. Once the timer has run 10 times, we cancel it using the timer’s upstream connection.

import SwiftUI

struct TimersSwiftUIPlayground: View {
    private let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
    
    @State private var timerRunCount = 0
    
    var body: some View {
        ScrollView {
            Text(String(timerRunCount))
                .onReceive(timer) { _ in
                    timerRunCount += 1
                    
                    guard timerRunCount > 9 else { return }
                    timer.upstream.connect().cancel()
                }
        }
    }
}

Non-repeating Timer

For non-repeating timers, we can use either a closure or a method selector to define the code to be executed. Similarly, we can create repeating timers using the scheduledTimer method and specifying the time interval between each execution.

// Timer with closure
let timer1 = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { timer in
    print("forceUnwrap.com - have a nice day")
}

// Timer calling method
let timer2 = Timer.scheduledTimer(
    timeInterval: 1.0,
    target: self,
    selector: #selector(fireTimer),
    userInfo: nil,
    repeats: false
)

@objc
func fireTimer() {
    print("forceUnwrap.com - you will learn something new today")
}

Repeating Timer

// Timer with closure
let timer1 = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
    print("forceUnwrap.com - You are beautiful")
}

// Timer calling method
let timer2 = Timer.scheduledTimer(
    timeInterval: 1.0,
    target: self,
    selector: #selector(fireTimer),
    userInfo: nil,
    repeats: true
)

@objc
func fireTimer() {
    print("forceUnwrap.com - You will learn something new today")
}

Termination

It’s a good practice to store timers in properties so we can terminate them when needed. We can do this by calling the timer’s invalidate method or setting its value to nil. Additionally, we can use the tolerance property to optimize for power savings and responsiveness.

// Timer with closure
var timerRunCount = 0

Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in
    print("forceUnwrap.com -> You can do this ")
    timerRunCount += 1
    
    guard timerRunCount == 5 else { return }
    timer.invalidate()
}

// Timer calling method
var timer: Timer?
var timerRunCount = 0

let timer = Timer.scheduledTimer(
    timeInterval: 1.0,
    target: self,
    selector: #selector(invalidateTimerIfNeeded),
    userInfo: nil,
    repeats: true
)

@objc
private func invalidateTimerIfNeeded() {
    print("forceUnwrap.com -> You can do this ")
    timerRunCount += 1
    
    guard timerRunCount == 5 else { return }
    timer?.invalidate()
}

Tolerance

Tolerance helps the system to optimize for increased power savings and responsiveness. Apple documentation.

let timer = Timer.scheduledTimer(
    timeInterval: 1.0,
    target: self,
    selector: #selector(fireTimer),
    userInfo: nil,
    repeats: true
)
timer.tolerance = 0.3

@objc
private func fireTimer() {
    print("forceUnwrap.com")
}

Run loops

When creating timers on the main thread, we may encounter issues with user interaction and UI updates. To avoid these problems, we can use the RunLoop.current method to add the timer to the main thread’s run loop.

Timer issue with user interaction
let timer = Timer.scheduledTimer(
    timeInterval: 1.0,
    target: self,
    selector: #selector(fireTimer),
    userInfo: nil,
    repeats: true
)
RunLoop.current.add(timer, forMode: .common)

@objc
private func fireTimer() {
    print("forceUnwrap.com")
}
Timer solution

Sync with screen update

Alternatively, we can sync the timer with the display refresh rate using the CADisplayLink class.

let displayLink = CADisplayLink(target: self, selector: #selector(update))
displayLink.add(to: .current, forMode: .default)

@objc
private func update() {
    print("forceUnwrap.com")
}

Overall, Timer is a useful class for scheduling and executing code on a periodic basis in Swift.

The post Probably the best guide to Swift Timer first appeared on forceUnwrap.

]]>
https://forceunwrap.com/probably-the-best-guide-to-swift-timer/feed/ 0
Pass some View as parameter to another View https://forceunwrap.com/pass-some-view-as-parameter-to-another-view/ https://forceunwrap.com/pass-some-view-as-parameter-to-another-view/#respond Fri, 26 Aug 2022 10:53:43 +0000 https://forceunwrap.com/?p=237
import SwiftUI

struct CustomNavView<Content: View>: View {
    let content: Content
    let title: String
    
    init(title: String, @ViewBuilder content: () -> Content) {
        self.content = content()
        self.title = title
    }
    
    var body: some View {
        NavigationLink(destination: self.content) {
            Text(title)
        }
        .padding()
    }
}

Usage:

CustomNavView(title: "Picker sample in SwiftUI") {
    EmptyView()
}

Changelog:

// Good stuff. 🙇‍♂
git commit -m "Update to trailing closure as head_of_roses recommends" 

The post Pass some View as parameter to another View first appeared on forceUnwrap.

]]>
https://forceunwrap.com/pass-some-view-as-parameter-to-another-view/feed/ 0
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
Picker using struct in SwiftUI https://forceunwrap.com/picker-using-struct-in-swiftui/ https://forceunwrap.com/picker-using-struct-in-swiftui/#comments Sun, 07 Aug 2022 11:50:51 +0000 https://forceunwrap.com/?p=212 I have observed that there are numerous tutorials online that explain how to set up Picker in SwiftUI. However, most of them utilize a simple string array for data, which is not sufficient for more complex scenarios. Instead, you should use a struct or class. In this example, I will demonstrate how to use an array of custom structs.

Here is a sample code that shows how to create a Picker with custom struct data:

import SwiftUI

struct ContentView: View {
    private var cars = [
        Car(brand: "BMW", plates: "AAA111"),
        Car(brand: "Toyota", plates: "BBB222"),
        Car(brand: "Kia", plates: "CCC222")
    ]

    @State private var selectedCar = ""

    var body: some View {
        VStack {
            Picker("Car", selection: $selectedCar) {
                ForEach(cars, id: \.brand) {
                    Text("\($0.brand) \($0.plates)")
                }
            }
            Text("You selected: \(selectedCar)")
        }
    }
}

struct Car: Hashable, Identifiable {
    var id: String {
        return brand + plates
    }
    
    let brand: String
    let plates: String
}

In this example, the Car struct is used as the data source for the Picker. The selectedCar variable is now of type Car? to hold the selected car, instead of a String. Also, the id property has been removed from the Car struct because it is no longer necessary.

The accessibility modifier is used to provide an invisible label to the Picker for VoiceOver users. This ensures that the Picker is accessible to all users.

Finally, I removed the id argument in the ForEach because the Car struct is now identifiable by default. Therefore, you can use car directly instead of car.brand to reference the selected Car object.

            Picker("Car", selection: $selectedCar) {
                ForEach(cars) {
                    Text("\($0.brand) \($0.plates)")
                }
            }

The post Picker using struct in SwiftUI first appeared on forceUnwrap.

]]>
https://forceunwrap.com/picker-using-struct-in-swiftui/feed/ 2
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