native - forceUnwrap https://forceunwrap.com Lift your Swift skills & career to the next level Fri, 29 Sep 2023 19:58:37 +0000 en-US hourly 1 https://wordpress.org/?v=7.0 https://forceunwrap.com/wp-content/uploads/2023/03/cropped-ForceUnwrap-32x32.jpg native - forceUnwrap https://forceunwrap.com 32 32 Swift multiple trackers architecture with SOLID – Part 2 https://forceunwrap.com/swift-multiple-trackers-architecture-with-solid-part-2/ https://forceunwrap.com/swift-multiple-trackers-architecture-with-solid-part-2/#respond Sun, 19 Mar 2023 18:06:12 +0000 https://forceunwrap.com/?p=371 In part 1, we developed code to implement an infrastructure that enables multiple trackers. In part 2, we will refactor our code based on SOLID principles to improve its design.

SOLID principles consist of five guidelines that help developers create code that is easy to maintain, extend and test. These principles include:

  • Single Responsibility Principle (SRP): Our code follows SRP as the TrackerStore class is responsible for storing and registering trackers, while the Tracker class is responsible for implementing tracking functions.
  • Open/Closed Principle (OCP): Our code follows OCP by allowing the system to be extended without modifying existing code, thanks to the TrackerConfigurable protocol that enables/disables trackers through configuration.
  • Liskov Substitution Principle (LSP): Our code follows LSP as the AdobeTracker, BrazeTracker, and SumoTracker classes implement their respective protocols without violating the behavior expected by their supertypes.
  • Interface Segregation Principle (ISP): Our code follows ISP by using separate protocols for each set of related functions (PlaybackTrackable, NavigationTrackable, OptionalNavigationTrackable) instead of one large protocol.
  • Dependency Inversion Principle (DIP): Our code follows DIP as the Tracker class depends on abstractions (protocols) rather than concrete types, allowing for easier testing and swapping of implementations.
enum TrackerType {
    case braze, adobe, sumo
}

extension TrackerType {
    var isEnabled: Bool {
        return TrackerConfig.enabledTrackers.contains(self)
    }
}

// DIP
// Client depends on abstractions
protocol Trackable {
    var type: TrackerType { get }
}

// OCP
// Enable/Disable trackers through configuration
protocol TrackerConfigurable {
    static var enabledTrackers: [TrackerType] { get }
}

struct TrackerConfig: TrackerConfigurable {
    static let enabledTrackers = [TrackerType.braze, TrackerType.adobe]
}


// SRP
// Tracker class is responsible for storing and registering trackers
final class TrackerStore {
    private(set) var trackers = [Trackable]()

    func registerTracker(tracker: Trackable) {
        if !trackers.contains(where: { $0.type == tracker.type }) {
            trackers.append(tracker)
        } else {
            print("🔥🔥🔥 We don't want duplicates 🔥🔥🔥")
        }
    }
}

// Tracker class is responsible for implementing tracking functions
class Tracker {
    private let trackerStore: TrackerStore

    init(trackerStore: TrackerStore) {
        self.trackerStore = trackerStore
    }
}

extension Tracker: PlaybackTrackable {
    private var playbackTrackers: [PlaybackTrackable] {
        trackerStore.trackers
            .filter { $0.type.isEnabled }
            .compactMap { $0 as? PlaybackTrackable }
    }

    func trackPlay() {
        playbackTrackers
            .forEach { $0.trackPlay() }
    }

    func trackPause() {
        playbackTrackers
            .forEach { $0.trackPause() }
    }

    func trackStop() {
        playbackTrackers
            .forEach { $0.trackStop() }
    }

    func trackNext() {
        playbackTrackers
            .forEach { $0.trackNext() }
    }

    func trackPrevious() {
        playbackTrackers
            .forEach { $0.trackPrevious() }
    }
}

extension Tracker: NavigationTrackable {
    private var navigationTrackers: [NavigationTrackable] {
        return trackerStore.trackers
            .filter { $0.type.isEnabled }
            .compactMap { $0 as? NavigationTrackable }
    }

    func trackNavigate(toView: String, fromView: String) {
        navigationTrackers
            .forEach { $0.trackNavigate(toView: toView, fromView: fromView) }
    }

    func trackBackTap(fromView: String) {
        navigationTrackers
            .forEach { $0.trackBackTap(fromView: fromView) }
    }

    func trackSettingsOpen(fromView: String) {
        navigationTrackers
            .forEach { $0.trackSettingsOpen(fromView: fromView) }
    }
}

extension Tracker: OptionalNavigationTrackable {
    private var optionalNavigationTrackers: [OptionalNavigationTrackable] {
        return trackerStore.trackers
            .filter { $0.type.isEnabled }
            .compactMap { $0 as? OptionalNavigationTrackable }
    }

    func optionalTrackTap() {
        optionalNavigationTrackers
            .forEach { $0.optionalTrackTap() }
    }
}

protocol PlaybackTrackable {
    func trackPlay()
    func trackPause()
    func trackStop()
    func trackNext()
    func trackPrevious()
}

// ISP
// Separate protocols for each function
protocol NavigationTrackable {
    func trackNavigate(toView: String, fromView: String)
    func trackBackTap(fromView: String)
    func trackSettingsOpen(fromView: String)
}

// LSP
// Separate protocol for optional function
protocol OptionalNavigationTrackable {
    func optionalTrackTap()
}

class AdobeTracker: Trackable {
    let type = TrackerType.adobe
}

extension AdobeTracker: NavigationTrackable {
    func trackNavigate(toView: String, fromView: String) {
        // Specific implementation
    }

    func trackBackTap(fromView: String) {
        // Specific implementation
    }

    func trackSettingsOpen(fromView: String) {
        // Specific implementation
    }
}

extension AdobeTracker: OptionalNavigationTrackable {
    func optionalTrackTap() {
        // Specific implementation
    }
}

class BrazeTracker: Trackable {
    let type = TrackerType.braze
}

extension BrazeTracker: NavigationTrackable {
    func trackNavigate(toView: String, fromView: String) {
        // Specific implementation
    }

    func trackBackTap(fromView: String) {
        // Specific implementation
    }

    func trackSettingsOpen(fromView: String) {
        // Specific implementation
    }
}

class SumoTracker: Trackable {
    let type = TrackerType.sumo
}

extension SumoTracker: OptionalNavigationTrackable {
    func optionalTrackTap() {
        // Specific implementation
    }
}

By following SOLID principles, we have created a well-structured code that is maintainable, extensible, and testable.

The post Swift multiple trackers architecture with SOLID – Part 2 first appeared on forceUnwrap.

]]>
https://forceunwrap.com/swift-multiple-trackers-architecture-with-solid-part-2/feed/ 0
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
SwiftLint on Apple Silicon M1 https://forceunwrap.com/swiftlint-on-apple-silicon-m1/ https://forceunwrap.com/swiftlint-on-apple-silicon-m1/#respond Sun, 12 Jun 2022 19:56:22 +0000 https://forceunwrap.com/?p=157 If you want to install SwiftLint, you can do so by running the following command in your terminal:

brew install swiftlint

Once you’ve installed SwiftLint, you can check where it’s located on your system by running:

which swiftlint
// For Apple Silicon -> /opt/homebrew/bin/swiftlint

If you’re using an Apple Silicon chip, SwiftLint may be located in the /opt/homebrew/bin directory. To ensure that SwiftLint is available in your build phases, you’ll need to add the following script to your Run Script phase:

# Build Phases -> Run script phase for Apple Silicon chips
# Adds support for Apple Silicon brew directory
export PATH="$PATH:/opt/homebrew/bin"

if which swiftlint; then
    swiftlint autocorrect && swiftlint
else
  echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi

This script adds support for the Apple Silicon brew directory and checks if SwiftLint is installed. If SwiftLint is installed, it will autocorrect and run it. Otherwise, it will display a warning message and provide a link to download SwiftLint from GitHub.

For those who wish to bypass the autocorrect function, simply replace swiftlint autocorrect && swiftlint with swiftlint.

If you encounter the error Sandbox: swiftlint(xxxxx) deny(1) file-read-data xxx /.swiftlint, navigate to Build settings and set the User Script Sandboxing value to “No”.

The post SwiftLint on Apple Silicon M1 first appeared on forceUnwrap.

]]>
https://forceunwrap.com/swiftlint-on-apple-silicon-m1/feed/ 0