• Skip to main content
  • Skip to primary sidebar

Ravi Shankar

Lifelong Learner

  • About
  • Portfolio

Expense Split – A Journey from Old to New

By Ravi Shankar

I recently decided to give my hobby app – Expense Split a complete makeover. The objective was not just to update its look but also to improve its architecture and add new features. In this blog post, I will walk you through the entire process, comparing old screenshots with new ones to highlight the changes.

1. A fresh new look

2. Core Data modifications with migration

3. Transition to SwiftUI

4. Implement MVVM architecture

5. Localisation support

6. UI and Unit Testing

7. Analytics Integration

8. In-app purchases

9. User feedback collection via Google Sheet

 A Fresh New Look

The first thing you will notice is that is the Updated UI. I wanted to make the app visually appealing, minimalistic but keeping the user experience intuitive.

Before

After

Core Data Modifications with Migration

The app initially used a somewhat cluttered Core Data model with multiple entities that had overlapping responsibilities. The helper methods for interacting with Core Data were also scattered and inconsistent.

I reduced the number of entities by merging some that had overlapping functionalities. I also refactored the Core Data helper methods to make them more efficient and easier to manage. Importantly, I implemented data migration to ensure that existing users wouldn’t lose any data during this transition.

Transition to SwiftUI

The app was originally built using UIKit/Storyboard, which was functional but a bit outdated. Switching to SwiftUI allowed me to create a more modern and interactive UI with less code.

Implementing MVVM Architecture

The app initially used MVC, which made it challenging to manage as the codebase grew. I switched to MVVM architecture to separate the business logic from the view, making the code easier to manage and test.

Localisation Support

I added localisation support to make the app accessible to a global audience. Now, the app automatically adapts to the language settings of the user’s device. Supported language are English, French, Japanese, German, Russian, Chinese, Hindi and Tamil. SwiftUI makes life easy for reading strings from the localisation file.

UI and Unit Testing

I implemented both UI and Unit tests to ensure that the app works as expected under various conditions. While I haven’t yet achieved 100% code coverage, the exercise was incredibly educational. I learned about various testing jargons like “mocks,” “stubs,” and “test suites,” and how they can be applied to improve the app’s reliability and maintainability.

Analytics Integration

I integrated analytics to track user behaviour, which will help in future updates.

I’m tracking several key metrics to understand user engagement and feature usage:

User Onboarding Completion: To see how many users successfully complete the onboarding process.

Migration Completion for Old Users: To ensure that users who are updating the app experience a smooth transition.

CSV Export Attempts: To gauge the usage of the app’s export feature.

Number of Expenses Created: To understand how actively the app’s core functionality is being used.

In-App Purchases

To monetise the app, I implemented in-app purchases, allowing users to buy premium features. Now the data export to CSV is available as an In-app purcahse.

User Feedback via Google Sheet

I added an option for users to provide feedback directly through a form that populates a Google Sheet. This makes it easier to collect and analyse user opinions for future improvements.

Conclusion

Revamping my hobby app was a rewarding experience. Not only did it get a visual upgrade, but it also gave exposure to analytics integration, Unit and UI testing, refactoring etc. Hope this post gives you some insights and inspiration for your own app development journey.

You can check out the Easy Split App

Filed Under: App Reviews, Expense Split

Closures, Extensions and Generics in Swift

By Ravi Shankar Leave a Comment

Closures

Closures are self contained lines of code that can be passed around the application and similar to blocks in Objective-C. A typical closure syntax in Swift looks as shown below

Closure Syntax

{ (parameters) -> return type in
  statements
}

Example closure in Swift

var greetings = { (name:String, message:String) -> (String) in
   message + " " + name + " !!!"
}

greetings("Ravi","Welcome")

In the above code example, a closure has been assigned to a variable. The purpose of this closure is to concatenate the string parameters and return the appended message as return parameter.

Type Inference

The example closure can modified to ignore to the parameter types and closure supports type inference.

var greetings = { (name, message) -> (String) in
    return message + " " + name + " !!!"
}
greetings("Ravi","Welcome")

Implicit return

In single expression closure, you can omit the return keyword.

var greetings = { (name, message) -> (String) in
    return message + " " + name + " !!!"
}
greetings("Ravi","Welcome")

var numbers = [23,45,67,89,89,78]

numbers.sort {
    (number1, number2) -> Bool in return number1 < number2
}

numbers.sort { number1, number2 in return number1 < number2 }
numbers.sort { number1, number2 in number1 < number2 }

//Shorthand argument syntax
numbers.sort { $0 < $1 }

Shorthand Argument Syntax

Swift supports shorthand argument names for inline closures. In the above example used for implicit returns, the two parameters can be removed and represented in shorthand arguments as shown below.

numbers.sort { $0 < $1 }

Trailing Closure

In a function with closure as the last parameter, the closure can be treated as trailing closures i.e closures outside the function parenthesis call. This is quite helpful in reducing the long closure expression. For example, the sorted function has closure as the last parameter and with trailing closure this becomes as shown below.

var numbers = [23,45,67,89,89,78] 
var sortedNumbers = sorted(numbers, {$0 > $1}) 
// Without trailing closure 
 var sortedNumbers = sorted(numbers) {$0 > $1} 
// represented as trailing closure 
sortedNumbers

Extensions

Swift extensions are similar to category in Objective-C which adds new functionally to existing class, enumeration or Struct. Extension does not require the source code of original class or enumeration type or struct to extend their functionality.

Listed below is an example which extends String class. A new function fromDouble has been added to String class which takes a double value and returns String.

extension String {
    static func fromDouble(doubleValue: Double) -> String {
        var temp = String(format: "%.2f", doubleValue)
        return temp as String
    }
}

String.fromDouble(doubleValue:24.50)

Generics

Generics are code that produces the same result irrespective of the data type. Listed below is a function that accepts two numbers and swaps the values.

func swapValues(first: inout Int, second: inout Int) {
    let temp = first
    first = second
    second = temp
}

var number1 = 10
var number2 = 3

swapValues(first:&number1, second: &number2)

Now if we want to use the same function for swapping string values then we will have to re-write the function. Instead we can use Generics to use the function for any types. Generics solves the problem of having different set of code for different data types by implementing the functionality for a generic type.

func swapValues<T>(first: inout T, second: inout T) {
    let temp = first
    first = second
    second = temp
}

var first = "Ravi"
var second = "Satish"

swapValues(first:&first, second: &second)

first
second

Filed Under: Develop, Interview Questions, ios, Programming Tagged With: Closures, Extensions

Dependency Injection in Swift

By Ravi Shankar

Dependency injection is a design pattern that lets you to pass the dependencies for the object instances instead of creating the them inside the instances. Let us see this with an example of an app that manages Expenses.

In an Expense app we might have different components like

ExpenseManager – Responsible of managing business logic of expenses like creating, retrieving and deleting expenses.

ExpenseRepository – Responsible for data persistence of expenses, this interacts with database or network APIs to persists data.

ExpenseViewController – This is the UI component responsible for displaying Expenses and provides user interaction with the user.

ExpenseManager has a dependency on ExpenseRepository for saving and retrieving expense details. If we don’t use Dependency injection then ExpenseManager has to create an instance of ExpenseRepository and will be tightly coupled with each other.

class ExpenseManager {
  private let repository = ExpenseRepository()
}

Instead we can create a separate protocol for ExpenseRepository with saveExpense and getExpenses functions. ExpenseManager depends on ExpenseRepositoryProtocol which is provided through initialiser. ExpenseRepository has to conform with ExpenseRepositoryProtocol and provides the implementation. ExpenseViewController depends on ExpenseManager and it is passed in the initialiser.

// ExpenseRepository protocol
protocol ExpenseRepositoryProtocol {
    func saveExpense(_ expense: Expense)
    func getExpenses() -> [Expense]
}

// ExpenseManager
class ExpenseManager {
    private let repository: ExpenseRepositoryProtocol
    
    init(repository: ExpenseRepositoryProtocol) {
        self.repository = repository
    }
    
    func addExpense(_ expense: Expense) {
        repository.saveExpense(expense)
    }
    
    func getAllExpenses() -> [Expense] {
        return repository.getExpenses()
    }
}

// ExpenseRepository implementation
class ExpenseRepository: ExpenseRepositoryProtocol {
    private var expenses: [Expense] = []
    
    func saveExpense(_ expense: Expense) {
        expenses.append(expense)
    }
    
    func getExpenses() -> [Expense] {
        return expenses
    }
}

// ExpenseViewController
class ExpenseViewController {
    private let expenseManager: ExpenseManager
    
    init(expenseManager: ExpenseManager) {
        self.expenseManager = expenseManager
    }
    
    func addExpense(_ expense: Expense) {
        expenseManager.addExpense(expense)
    }
    
    func displayExpenses() {
        let expenses = expenseManager.getAllExpenses()
        // Display expenses in the UI
    }
}

Benefits of Dependency Injection

  • Loose Coupling – Each component depends only on the protocols allowing for flexibility and easier substitution of implementation.For example if you want to change the persistence from Core Data to Firebase, you have to make sure the Firebase Persistence class conforms to ExpenseRepositoryProtocol and ExpenseManager doesn’t need to know about the Firebase Persistence class.
  • Testability – With Dependency Injection it becomes easier to write unit tests by providing mock or stub implementations.
  • Scalability and Maintainability – This promotes modular design, as it is easier to add or replace components without impacting the entire system.

Filed Under: Design Pattern, Interview Questions, ios

Memory management in Swift

By Ravi Shankar Leave a Comment

Memory management in Swift is done by Automatic Reference Counting or ARC. Whenever a variables holds an instance of an object the memory count for that object increases by 1. And when variable becomes out of scope or set to nil, the memory count decreases 1.

class Teacher {
    var name: String?
    var course: String?

    init (name: String, course: String) {
        self.name = name
        self.course = course
        print("Reference count increased by 1")
    }

    deinit{
        print("Reference count decreased by 1")
    }
}

let teacher1 = Teacher(name: "Ravi", course: "Swift")

func createTeacher() {
    let teacher2 = Teacher(name: "John", course: "Java")
}

createTeacher()

In the above example, we are creating two instances of Teacher class and storing it in variables teacher1 and teacher2. Since teacher2 variable is created within the function, it becomes out of scope after the function call. You should be able to observe the two init messages and one deinit (teacher2) message in console log. This should give you some idea on how reference counting works in Swift.

Increasing and decreasing of reference count are automatically handled by ARC but problem occurs when we have a strong reference cycle. A strong reference cycle refers to cyclic relationship between the objects.

class Teacher {
    var name:String?
    var course:String?
    var student: Student?

    init(name: String, course:String) {
        self.name = name
        self.course = course

        print("Reference count of Teacher increases by 1")
    }

    deinit {
        print("Reference count of Teacher decreases by 1")
    }
}

class Student {
    var name:String?
    var mentor: Teacher?

    init(name: String, course:String) {
        self.name = name

        print("Reference count of Student increases by 1")
    }

    deinit {
        print("Reference count of Student decreases by 1")
    }
}

func createInstance() {
    let teacher = Teacher(name: "Jason", course: "Swift")
    let student = Student(name: "Adam", course: "Swift")
    teacher.student = student
    student.mentor = teacher
}

createInstance()

In the above code snippet, Teacher and Student classes have a strong reference cycle and both student and teacher instances remain in memory even after the end of function call. A strong reference cycle can be avoided by declaring any one of the instance as weak or unowned

weak var student: Student?

You can also unown the reference when you know the reference cannot be nil

unowned var mentor: Teacher

unowned var mentor: Teacher

Filed Under: Interview Questions, ios, Xcode Tagged With: ARC, iPad, Memory Management, Xcode

What are the different lifecycle methods in a typical UIViewController?

By Ravi Shankar Leave a Comment

Here is the list of Lifecycle methods in a ViewController are

viewDidLoad – Called after the view controller’s view hierarchy has been loaded into memory. It is used for initial setup, such as creating and configuring UI elements.

viewWillAppear – Called just before the view is about to be added to the view hierarchy and become visible. It is used to perform tasks that need to be done every time the view appears.

viewDidAppear – Called when the view has been fully transitioned onto the screen and is visible to the user. It is used to perform tasks that require the view to be on the screen.

viewWillDisappear – Called just before the view is removed from the view hierarchy and becomes invisible. It is used to perform cleanup or save data before the view disappears.

viewDidDisappear – Called when the view has been fully transitioned off the screen and is no longer visible. It is used to perform additional cleanup or reset any state as needed.

deinit – Called when the view controller is being deallocated from memory. It is used to perform final cleanup and release any resources held by the view controller.

Filed Under: Interview Questions, ios

Explain App Life Cycle

By Ravi Shankar Leave a Comment

The app life cycle refers to the sequence of steps that app takes when an user launches an app until it terminates. Understanding the life cycle will help the developer to manage app behaviour and proper allocation and deallocation of resources.


Image Credit :- Apple

Not Running – Initial stage when the app is not launched or it has been terminated by user or by the system.

Inactive – The app enters Inactive state when it is launched but does not receiving any user inputs.

Active – The app is in the foreground and receiving user inputs.

Background – App enters background state when the user switches to another app or home screen. Please note that the app will not directly transition from Active to Background state, it will first transition to Inactive then to Background state.

Suspended – App enters suspended state when system wants to free resources.

Terminate – App enters terminate state when it is no longer running.

Which state an app is during the following scenario? share your answer in the comment section.

  1. When the app is foreground and you receive a message notification.
  2. When the app is foreground and you receive a phone call and start attending.

Filed Under: Interview Questions, ios

Difference between Delegate and Notifications in iOS

By Ravi Shankar

DelegatesNotifications
One-to-one communicationOne-to-many or many-to-many communication
Customized behaviorBroadcasting information/events
Delegate object holds a referenceObserving objects don’t need references
Specific responsibilities/tasksWidely distributed information/events
Tight coupling between objectsLoose coupling between objects
Object needs to know its delegatePosting object doesn’t know receivers
Callbacks, data source protocols, event handlingApplication-wide event handling

Filed Under: Interview Questions, iOS Developer

Class and Struct in Swift

By Ravi Shankar 3 Comments

Download the playground file from github (Classes and Struct)

Class

A class is a blue print for a real-word entity such Player, Person etc. and it is used for creating objects. Class can have properties to store values and methods to add behaviour. Let us see this with an example class called Rectangle which has some properties and two methods for calculating area and for drawing a rectangle.

class Rectangle {

var name:String = ""
var length:Double = 0
var breadth:Double = 0

 func area() -> Double {
   return length * breadth
 }

 func draw() -> String {
   return "Draw rectangle with area \(area()) "
 }
}

let rect = Rectangle()

rect.length = 20
rect.breadth = 10
rect.draw()

In the above example, we have a class named Rectangle, with name, length and breadth as properties, area and draw are functions. rect is a instance variable or object of Rectangle class. On setting the length and breadth and calling draw function should provide the following output in Playground.

201505101309.jpg

Similarly the below code create a Square class

class Square {

 var name:String = ""
 var length:Double = 0

 func area() -> Double {
  return length * length
 }

 func draw() -> String {
  return "Draw a square with area \(area()) "
 }
}

let squr = Square()
squr.length = 20
squr.draw()

Now instead of repeating property and functions in each classes let us use class inheritance to simplify these classes.

Class Inheritance

Let us create a parent class called Shape and its properties and functions will be inherited by Sub Classes Rectangle and Square.

Parent Class – Shape

class Shape {
    var name: String = ""

    func area() ->Double {
        return 0
    }

    func draw() ->String {
        return "Draw a \(name) with area \(area()) "
    }
}

Sub Class – Square

class Square:Shape {
    var length: Double = 0

    override func area() ->Double {
        return length * length
    }
}

let squr = Square()
squr.name = "My Square"
squr.length = 5
squr.draw()

Sub Class – Rectangle

class Rectangle:Shape {
    var length: Double = 0
    var breadth: Double = 0

    override func area() -> Double {
        return length * breadth
    }
}

let rect = Rectangle()
rect.name = "My Rectangle"
rect.length = 5
rect.breadth = 10
rect.draw()

Parent class Shape has been created with name property and with functions area and draw. The child class Square and Rectangle will inherit these property and methods. Apart from the parent class property, Square can have its own property length and Rectangle has length and breadth.

The parent class area function has been overridden by Square and Rectangle class to calculate corresponding areas. Now if you want add one more Shape such as Triangle, Circle etc the new class has to inherit Parent class (Shape) and add its own property and methods (or override methods).

Initialisers

Initialisers in Class and Struct are used for setting the default values for properties and for doing some initial setup. Here is a typical example of initialiser in a Class where the name property is initialised at the time of creating an instance.

class Shape {
    var name: String
    
    init(name: String) {
        self.name = name
    }

    func area()-> Double {
        return 0
    }

    func draw()-> String {
        return "Draw a \(name) with area \(area())"
    }
}

class Square: Shape {
    var length: Double = 0

    init() {
        super.init(name: "MySquare")
    }

    override func area()-> Double {
        return length * length
    }

    override func draw()-> String {
        return "Draw a \(name) with area \(area())"
    }
}

let squr = Square()
squr.length = 10
squr.draw()

The sub class Square initialises the name property in init function by calling super.init and after creating the Square instance you need to pass value for the length property.

Designated and Convenience Initialisers

Initialiser which initialises all the properties in a class is known as designated initialiser. A convenience initialiser will initialise only selected properties and in turn will call the designated initialiser in init function. Listed below is a Square class with designated initialiser and convenience initialiser

class Shape {
    var name: String

    init(name: String) {
        self.name = name
    }

    func area()-> Double {
        return 0
    }

    func draw()-> String {
        return "Draw a \(name) with area \(area())"
    }
}

class Square: Shape {

    var length: Double

    // Designated Initialiser
    init(length:Double, name:String) {
        self.length = length
        super.init(name: name)
    }

    // Convenience Initialiser
    convenience init(length: Double) {
        self.init(length:length, name:"MySquare")
    }

    override func area()-> Double {
        return length * length
    }

    override func draw()-> String {
        return "Draw a \(name) with area \(area())"
    }
}

let squr = Square(length: 10,name: "MySquare")
squr.draw()

let squrNew = Square(length: 20)
squrNew.draw()

Computed Property

A property in swift can be used for performing operation at the time of assigning value. Here is an example, where the length of Square is computed based on assigned area.

class Square {
    var length: Double = 0
    
    var area: Double {
        get {
            return length * length
        }

        set (newArea) {
            self.length = sqrt(newArea)
        }
    }
}

let square = Square()
square.area = 4 // set call
square.length = 6
square.area // get call

lazy Property

Swift also provides lazy property whose value is assigned when the user access the property.

class Person {

    var name: String

    init (name: String) {
        self.name = name
    }


    lazy var message: String = self.getMessage()


    func getMessage()-> String {
        return "Hello \(name)"
    }
}

let person = Person(name: "Jason")
person.message

in the above code example, the value for message property is not set at the initialisation and will be set only when call the message property in person object. Some typical where you could due lazy property is when retrieving values from performance intensify operation such as Network or Read/Write.

Property Observers

Swift provides two property observers, willSet and didSet. These methods gets triggered when a value is about to be set for a property or after setting the property.

class Square {
    var length: Double = 0 {

        willSet(newLength) {
            print("Setting length \(self.length) to new length \(newLength)")
        }

        didSet {
            print("Length is modified - do some action here")
        }
    }

    var area: Double {
        get {
            return length * length
        }

        set (newArea) {
            self.length = sqrt(newArea)
        }
    }
}

let square = Square()
square.length = -6
square.area

In the above example, Square class length property has a willSet and didSet observers.

Struct

Struct and Class can both have properties, methods, protocols, extensions and initialisers. You can use struct to hold simple values and when you want to pass around those across your program.

struct GeoDetails {
    var country: String
    var ip: String
    var isp: String
    var latitude: Double
    var longitude: Double
    var timeZone: String

    init(country: String, ip: String, isp: String, latitude:Double, longitude:Double, timeZone:String) {
        self.country = country
        self.ip = ip
        self.isp = isp
        self.latitude = latitude
        self.longitude = longitude
        self.timeZone = timeZone
    }

    func description()-> String {
        return "Country " + self.country + ", ip " + self.ip + ", isp " + self.isp + ", latitude \(self.latitude), longitude \(self.longitude) "
    }
}

Download the playground file from github (Classes and Struct)

Filed Under: Apple, ios, Programming Tagged With: Apple, Classes, Structures, Swift

  1. Pages:
  2. 1
  3. 2
  4. 3
  5. 4
  6. 5
  7. 6
  8. 7
  9. ...
  10. 39
  11. »
  • Go to page 1
  • Go to page 2
  • Go to page 3
  • Interim pages omitted …
  • Go to page 39
  • Go to Next Page »

Primary Sidebar

  • Email
  • LinkedIn
  • Twitter

iOS Cheat Sheets

Download free copy of iOS Cheat Sheets

Copyright 2023 © rshankar.com

Terms and Conditions - Privacy Policy