Cache Implementation in Cross-Platform Swift Apps

Cache Implementation in Cross-Platform Swift Apps

Implement persistence in Cross-Platform Swift apps!

Introduction

Have you ever used a mobile app that remembered your login information or preferences, even after you closed the app? This is thanks to the magic of SharedPreferences in Android and UserDefaults in iOS.

šŸ‘€ In this article, we'll explore how these two features work to store and retrieve key-value data in a cross-platform Swift app. šŸ” We'll start with an overview of what SharedPreferences and UserDefaults are and how they differ from each other. šŸ’” Then, we'll dive into some examples of how to use them in your Android and iOS apps and show you how to make your app's data persistent even when the user closes the app.

Whether you're a seasoned mobile developer or just getting started with cross-platform development, understanding how to use SharedPreferences and UserDefaults is an essential skill. So, let's get started and explore how these features can make your app more user-friendly and efficient! šŸ™Œ

SharedPreference & UserDefaults

First, let's talk about SharedPreferences, which is a lightweight and efficient way to store key-value pairs in Android. To use SharedPreferences in a cross-platform Swift app, you can use a library such as SwiftyUserDefaults or UserDefaultsWrapper. These libraries provide a convenient and type-safe way to access SharedPreferences in Swift, using syntax similar to that of UserDefaults in iOS.

Speaking of UserDefaults, this is a built-in mechanism in iOS that allows apps to store and retrieve user preferences and other small amounts of data. To use UserDefaults in a cross-platform Swift app, you can create a wrapper class that abstracts away the platform-specific details and provides a unified API for accessing user defaults.

Prerequisite

If you havenā€™t installed the SCADE IDE, download the SCADE IDE and install it on your macOS system. The only prerequisite for SCADE is the Swift language (at least basics). Also, please ensure the Android emulator or physical device is running if you want to run SCADE apps on an Android device.

We will try to implement saving the key-value pairs in Cache for both Android & iOS specific environments. One of the common uses of persistence is saving the login result after successful login. We will build a simple login screen and try to save the result if the authentication is successful.

Source Code

Design a SCADE Page

Letā€™s quickly design a screen with input fields Userā€™s name & email. There will be a button to save the key-value pairs. Similarly, there will be other two labels and a button to fetch the key-value pairs stored in Cache.

Design page for Cache SCADE Example

Code for iOS

For the iOS platform, we will use UserDefaults in the Foundation package to store key-value pairs. UserDefaults is a built-in tool in iOS that provides a simple way to store small amounts of data persistently between app launches. It is commonly used to store user preferences, settings, or small pieces of data.

You can access the UserDefaults object anywhere in your SCADE IDE. To get started, you should create an instance of UserDefaults.

Letā€™s save the current name and email string text to some variables.

    var currEmailText = ""
    var currNameText = ""

    self.email_tb.onTextChange.append(
      SCDWidgetsTextChangeEventHandler { ev in
        currEmailText = ev!.newValue
      })

    self.name_tb.onTextChange.append(
      SCDWidgetsTextChangeEventHandler { ev in
        currNameText = ev!.newValue
      })

Then, attach the onClick listener to the Save button and call the saveTocache() method which saves the key-value pairs to persistence.

    self.save_btn.onClick { _ in
      print("Email: \(currEmailText) Name: \(currNameText)")
      if currEmailText.isEmpty || currEmailText.isEmpty {
        print("Name or Email is empty")
        return
      }
      self.saveToCache(email: currEmailText, name: currNameText)
    }

As per the code snippet, it uses UserDefaults to save the persistence. Here, the keys are ā€œemailā€ and ā€œnameā€ which stores the values.

  func saveToCache(email: String, name: String) {
    #if os(iOS)
      UserDefaults.standard.set(email, forKey: "email")
      UserDefaults.standard.set(name, forKey: "name")
    #endif
  }

Now letā€™s fetch the Key-Value pairs

Now we will fetch the values for the keys stored in the Cache. Letā€™s write generic methods to get the values for the keys as Integer, String, and boolean values. It will use the same UserDefaults.standard.* property to get the values.

 func retrieveIntegerFromCache(key: String) -> Int {
    #if os(iOS)
      return UserDefaults.standard.integer(forKey: key) ?? 0
    #endif
    return 0
  }

  func retrieveStringFromCache(key: String) -> String {
    #if os(iOS)
      return UserDefaults.standard.string(forKey: key) ?? ""
    #endif
    return ""
  }

  func retrieveBoolFromCache(key: String) -> Bool {
    #if os(iOS)
      return UserDefaults.standard.bool(forKey: key) ?? false
    #endif
    return false
  }

Code for Android

We will now add the Android code in Swift to implement SharedPreferences. You can refer here for details about how the Android SDK SharedPreferences classes are mapped to the Swift equivalent.

Now letā€™s add the Android Swift library to the Package.swift.

// swift-tools-version:5.3

import Foundation
import PackageDescription

let SCADE_SDK = ProcessInfo.processInfo.environment["SCADE_SDK"] ?? ""

let package = Package(
  name: "ScadeProject",
  platforms: [
    .macOS(.v10_14)
  ],
  products: [
    .library(
      name: "ScadeProject",
      type: .static,
      targets: [
        "ScadeProject"
      ]
    )
  ],
  dependencies: [
    .package(
      name: "ScadeExtensions", url: "https://github.com/scade-platform/ScadeExtensions",
      .branch("main")),
    .package(
      name: "Android", url: "https://github.com/scade-platform/swift-android.git",
      .branch("android/24")),
  ],
  targets: [
    .target(
      name: "ScadeProject",
      dependencies: [
        .product(name: "Android", package: "Android", condition: .when(platforms: [.android])),
        .product(name: "AndroidOS", package: "Android", condition: .when(platforms: [.android])),
        .product(name: "AndroidApp", package: "Android", condition: .when(platforms: [.android])),
        .product(
          name: "AndroidContent", package: "Android", condition: .when(platforms: [.android])),
      ],
      exclude: ["main.page"],
      swiftSettings: [
        .unsafeFlags(["-F", SCADE_SDK], .when(platforms: [.macOS, .iOS])),
        .unsafeFlags(["-I", "\(SCADE_SDK)/include"], .when(platforms: [.android])),
      ]
    )
  ]
)

As the next step, import the Android packages and use the SharedPreferences methods to save/retrieve the key-value pairs.

#if os(Android)
  import Android
  import AndroidContent
  import AndroidApp
  import AndroidOS
  import Foundation
  import Java
#endif

We will now use the SharedPreferencesEditor which will be used to update/fetch the key-value pairs from the SharedPreferences.

  #if os(Android)
    private var currentActivity: Activity? { Application.currentActivity }
    var delegate: SharedPreferences?
    var delegateEditor: AndroidContent.SharedPreferencesEditor?
  #endif

Then, initialize the variables delegate and delegateEditor using the activity instance. Here, we will use the mode as 0(MODE_PRIVATE) to allow both read and write operations privately by application with the keyword-defined ā€œSCADEā€ instance.

    #if os(Android)
      delegate = Application.currentActivity?.getSharedPreferences(name: "SCADE", mode: 0)
      delegateEditor = delegate?.edit()
    #endif

Letā€™s write separate methods for saving and fetching the values using the delegateEditor instance.

Saving the Key-Value pairs

    public func putInt(value: Int32, key: String) {
        let val32:Int32 = value
        delegateEditor!.putInt(key: key, value: val32)
        delegateEditor!.commit()
    }

    public func putBoolean(value: Bool, key: String) {
        delegateEditor!.putBoolean(key: key, value: value)
        delegateEditor!.commit()
    }

    public func putString(value: String, key: String) {
        delegateEditor!.putString(key: key, value: value)
        delegateEditor!.commit()
    }

    public func putFloat(value: Float, key: String) {
        delegateEditor!.putFloat(key: key, value: value)
        delegateEditor!.commit()
    }

Fetching the Key-Value pairs

    public func getInt(key: String) -> Int32 {
        let intValue:Int32 = (delegate?.getInt(key: key, defValue: 0)) ?? 0
        return intValue
    }

    public func getString(key: String) -> String {
        let strValue:String = delegate?.getString(key: key, defValue: "DEFAULT_VALUE_STRING")  ?? ""
        return strValue
    }

    public func getBoolean(key: String) -> Bool {
        let boolValue:Bool = delegate?.getBoolean(key: key, defValue: false)  ?? false
        return boolValue
    }

    public func getFloat(key: String) -> Float {
        let floatValue:Float = delegate?.getFloat(key: key, defValue: 0.0)  ?? 0.0
        return floatValue
    }

    public func containsKey(key: String) -> Bool {
        return delegate!.contains(key: key)
    }

Now we will use the above methods wherever required to save and fetch the values for the keys: ā€œemailā€ & ā€œnameā€. Hence the below method is used to update the values.

  func retrieveStringFromCache(key: String) -> String {
    #if os(iOS)
      return UserDefaults.standard.string(forKey: key) ?? ""
    #endif

    #if os(Android)
      return self.getString(key: key)
    #endif

    return ""
  }

After fetching the values stored in the applicationā€™s cache, update the text for name & email labels.

    self.fetch_btn.onClick { _ in
      print(
        "retrieve from cache: \(self.retrieveStringFromCache(key: "email")) \(self.retrieveStringFromCache(key: "name"))"
      )

      self.email_fetch.text = "Email: \(self.retrieveStringFromCache(key: "email"))"
      self.name_fetch.text = "Name: \(self.retrieveStringFromCache(key: "name"))"

    }

Run the App on iOS/Android

In order to run the app on iOS/Android devices, please ensure the physical devices or simulator/emulator are up and running. SCADE apps can be run on SCADE emulators, iOS & Android devices as well as Apple and Android emulators.

You can also build the app for Android (APK/AAB) or iOS (IPA) to publish them to the respective app stores. You need to click on the App Name button and choose the device target accordingly.

iOS

iOS Cache SCADE

Android

Android Cache SCADE

Voila šŸŽ‰! It was really interesting to create an iOS or Android app using the SCADE IDE by implementing Cache. It is really cool. We encourage you to implement SharedPreferences with SCADEšŸ˜Š.

Ā