Guides - 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 Guides - 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
Swift multiple trackers architecture – part 1 https://forceunwrap.com/swift-multiple-trackers-architecture/ https://forceunwrap.com/swift-multiple-trackers-architecture/#comments Fri, 17 Feb 2023 10:29:48 +0000 https://forceunwrap.com/?p=287
Overview

Tracking is an essential part of app development. It allows us to measure the performance, behavior and preferences of our users and optimize our app accordingly. However, tracking can also be a challenge when we have to deal with multiple trackers from different providers. How do we keep our code clean, maintainable and testable when we have to integrate and update various trackers in our app?

In this article, I will show you how to use Swift Package Manager (SPM) to modularize your tracking code and make it easier to manage multiple trackers in your app. SPM is a tool that helps us create and distribute Swift packages that contain source code and resources for our projects. By using SPM, we can separate our tracking code into different packages that can be reused, tested and updated independently.

We will start by creating an abstract tracker class that will be responsible for managing all the trackers in our app. Then we will create different tracker implementations that conform to a common protocol and register them with the abstract tracker class. Finally, we will see how to use SPM to create and import these tracker packages into our main app project.

Let’s get started!

The idea

Let’s create a general Tracker class responsible for managing all trackers, without containing any specific tracker implementation. The Tracker class will allow us to register a Trackable tracker, which will then be added to the trackers array. Ideally, the trackers array should not be accessible from outside the class, but due to the need for using extensions for simpler code management in different files, we are forced to use private(set). This allows us to read the array, but we cannot write to it outside the scope of the Tracker class. If you have any suggestions on how to improve this, please let me know in the comments below. We can all benefit from the knowledge since this is one of the known drawbacks at the moment.

protocol Tracking {
    func registerTracker(tracker: Trackable)
}

// General implementation
public class Tracker: Tracking {
    // Array of our trackers
    // Is there a way to store objects faster and without duplicates?
    private(set) var trackers = [Trackable]()

    // A way to register a Trackable, without duplicates
    public func registerTracker(tracker: Trackable) {
        if !trackers.contains(where: { $0.type == tracker.type }) {
            trackers.append(tracker)
        } else {
            print("🔥🔥🔥 We don't want duplicates 🔥🔥🔥")
        }
    }
}

In order to make our tracker masterplan effective, it’s essential to identify the type of tracker we’re working with. Protocols provide a solution to this problem. I’ve named our protocol Trackable, which contains the TrackerType. Having the TrackerType enables us to enable/disable specific trackers or perform other magic tricks with them. It’s like ALOHOMORA, unlocking a whole new level of potential for our trackers!

Alternatively, we could skip the TrackerType and allow specific trackers to simply do their job without worrying about whether they’re enabled or not. This approach may be more straightforward, but it may also limit our ability to customize and fine-tune the behavior of our trackers. Ultimately, the decision of whether to use TrackerType or not will depend on our specific needs and use case.

@objc public protocol Trackable {
    var type: TrackerType { get }
}

@objc public enum TrackerType: Int {
    case general, braze, sumo, adobe

    var isEnabled: Bool {
        switch self {
        case .general, .braze, .adobe:
            return true
        case .sumo:
            return false
        }
    }
}

One potential drawback of having a general TrackerType is that it may feel unnecessary in certain cases. However, this is a matter of debate, and if you have any ideas on how to improve this, let’s have a discussion. The question of whether the Tracker should have a type is an important one that deserves careful consideration. While a TrackerType may provide some benefits, such as enabling us to enable/disable specific trackers or perform other customizations, it may also add unnecessary complexity to our code. Ultimately, the decision of whether to include a TrackerType or not will depend on our specific needs and the goals of our project.

Optional func

// Need the protocol to be @objc, so we could use @objc optional
@objc public protocol TrackableHome {
    // Optional track func, not necessarily implemented by all trackers
    @objc optional func trackHomeTap(extraParam: String)
    func trackNavigate(toView: String, fromView: String)
    func trackLogin()
}

The @objc optional keyword allows us to define functions that may not be implemented by all specific trackers, but only by some of them. This can be very useful when we want to provide flexibility in our tracking system, allowing each tracker to implement only the functions that are relevant to its specific needs. Without @objc optional, we would need to define each function in every tracker, even if it wasn’t used, which could lead to bloated and inefficient code.

Event

struct Event: Eventable {
    let name: String
    let parameters: [String: String]
}

protocol Eventable {
    var name: String { get }
    var parameters: [String: String] { get }
}

Specific tracker protocols

// Need the protocol to be @objc, so we could use @objc optional
@objc public protocol TrackableHome {
    // Optional track func, not necessarily implemented by all trackers
    @objc optional func trackHomeTap(extraParam: String)
    func trackNavigate(toView: String, fromView: String)
    func trackLogin()
}

public protocol TrackablePlayback: Trackable {
    func trackPlay()
    func trackPause()
    func trackNext()
    func trackPrevious()
}

We have a couple of different protocols above. One is for tracking homepage events, other is for tracking playback events. But we could have as many different protocols conforming to Trackable as we want. For each ViewController, feature, service…

Our goal is for our Tracker to support them, so that specific Trackable trackers could implement their specific implementations as well. We extend our abstract Tracker with TrackableHome. Next, we go through each registered tracker, check if it is supported(enabled) and call the same tracking function. The abstract tracker knows nothing about the specific implementations required by each tracker.

// Extension of Tracker, where we see if tracker is enabled and call all the enabled trackers
extension Tracker: TrackableHome {
    public func trackNavigate(toView: String, fromView: String) {
        trackers.forEach { tracker in
            guard tracker.type.isEnabled,
                  let tracker = tracker as? TrackableHome
            else {
                return
            }

            tracker.trackNavigate(toView: toView, fromView: fromView)
        }
    }

    public func trackLogin() {
        trackers.forEach { tracker in
            guard tracker.type.isEnabled,
                  let tracker = tracker as? TrackableHome
            else {
                return
            }
            
            tracker.trackLogin()
        }
    }
}

Specific tracker implementations

Let’s take a closer look at the implementation of a specific tracker. In the BrazeTracker class, our goal is to set up everything we need to run the tracker. By extending the class with Trackable, we gain support for TrackerType, which allows us to easily enable or disable trackers as needed.

To use BrazeTracker with a specific feature or service, we extend it with TrackableHome (or another relevant protocol). The Tracker class does not need to know the specific implementation details required by AppBoy – we can provide all of the necessary extras in the BrazeTracker class. This allows us to customize the specific tracker for our specific needs while still leveraging the benefits of the single Tracker class approach.

class BrazeTracker: Trackable {
    private let apikey = "YOUR-API-KEY"

    let type: TrackerType = .braze
//    let appboy = Appboy.sharedInstance()
}

extension BrazeTracker: TrackableHome {
    func trackNavigate(toView: String, fromView: String) {
        // Add specific event params or whatever is needed
        // Can be a specific event as well.
//        let event = Event(
//            name: "navigate",
//            parameters: [
//                "from": fromView,
//                "to": toView
//            ]
//        )
        // Call specific implementation
        // Sumo.track()
    }

    func trackLogin() {}

    func trackHomeTap(extraParam: String) {
        // Implements optional function
    }
}
class AdobeTracker: Trackable {
    var type = TrackerType.adobe
}

extension AdobeTracker: TrackableHome {
    func trackNavigate(toView: String, fromView: String) {
        // Add specific event params or whatever is needed
        // Can be a specific event as well.
//        let event = Event(
//            name: "navigate",
//            parameters: [
//                "from": fromView,
//                "to": toView
//            ]
//        )
        // Call specific implementation
        // Sumo.track()
    }

    func trackLogin() {}

    // Does not implement optional func
    // trackHomeTap(extraParam: String)
}
class SumoTracker: Trackable {
    let type: TrackerType = .sumo
}

extension SumoTracker: TrackableHome {
    func trackNavigate(toView: String, fromView: String) {
        // Add specific event params or whatever is needed
        // Can be a specific event as well.
        let event = Event(
            name: "navigate",
            parameters: [
                "from": fromView,
                "to": toView
            ]
        )
        // Call specific implementation
        // Sumo.track()
    }

    func trackLogin() {}

// Does not implement optional func
// trackHomeTap(extraParam: String)
}

We also have implementations for other specific trackers using this setup, which makes it easy to add, disable, and maintain different trackers as needed. By abstracting away the implementation details and leveraging protocols like Trackable, we can create a flexible and modular tracking system that can be customized to suit the needs of any project.

Protocol naming tip for teams

When it comes to protocol naming in Swift, it’s important to follow some guidelines. The Swift.org documentation suggests using suffixes like -able, -ible, or -ing. However, it’s not always clear which suffix to use for a given protocol.

In some cases, a protocol may have multiple suffixes that sound good, like Eventable, Eventing, or Eventible. This can lead to confusion, especially for non-native English speakers.

One approach to simplify protocol naming is to simply add the word Protocol at the end of the protocol name, like EventProtocol. This approach can be helpful for ensuring consistency across a team and avoiding confusion during code reviews.

However, it’s important to make sure that all team members are on board with this approach and that it fits within the larger project’s naming conventions. Ultimately, the most important thing is to choose a naming convention that is clear, consistent, and easy to understand for everyone working on the project.

SOLID discussion and improvements

Does this code conform to SOLID? How could we improve it? What rules are followed partially? What are adhered?

If you have any suggestions on how to improve this implementation, please share them in the comments below. We can work together to refine the Tracker class and make it even more effective.

Don’t forget to read the follow-up, where we’ll be discussing how we’ve refactored the code according to SOLID principles in part 2.

The post Swift multiple trackers architecture – part 1 first appeared on forceUnwrap.

]]>
https://forceunwrap.com/swift-multiple-trackers-architecture/feed/ 1
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
Xcode 13 show tests code coverage https://forceunwrap.com/xcode-13-show-tests-code-coverage/ https://forceunwrap.com/xcode-13-show-tests-code-coverage/#respond Mon, 04 Jul 2022 07:31:30 +0000 https://forceunwrap.com/?p=181 Click target and “Edit scheme”.

Select “Test”. Tick the checkmark.

CMD + U – Run the test target.

CMD + 9 – Show the Report Navigator

When in Report navigator – expand the coverage.

The post Xcode 13 show tests code coverage first appeared on forceUnwrap.

]]>
https://forceunwrap.com/xcode-13-show-tests-code-coverage/feed/ 0
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