Logo
Published on

The Art of State Management in Swift for iOS

Authors
  • Name
    Twitter

State management is the essence of iOS applications. It determines how data flows within your app and how user interface elements reflect and react to changes. Swift, with its modern features, provides various ways to manage the state effectively. Let’s explore these methods and their best practices, along with some code examples to illustrate their implementation.

MVC (Model-View-Controller)

In an MVC architecture, the state is managed within the model and is communicated to the view via the controller

Example

class CounterModel {  
    var count = 0  
}  
  
class CounterViewController: UIViewController {  
    var counterModel = CounterModel()  
  
    func updateDisplay() {  
        countLabel.text = "\(counterModel.count)"  
    }  
  
    @IBAction func incrementCount(_ sender: UIButton) {  
        counterModel.count += 1  
        updateDisplay()  
    }  
}

Usage:

let counterVC = CounterViewController()  
counterVC.incrementCount(UIButton())

Delegation

Delegation allows you to pass data or state changes across different parts of the application, particularly from child to parent.

Example:

class DetailViewController: UIViewController {  
    weak var delegate: ItemSelectionDelegate?  
      
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {  
        delegate?.itemSelected("Item \(indexPath.row)")  
    }  
}

Usage:

let detailVC = DetailViewController()  
detailVC.delegate = self // Assuming 'self' conforms to 'ItemSelectionDelegate'

Singleton Pattern

Singletons serve as a global instance to manage app-wide state.

Example:

class AppStateManager {  
    static let shared = AppStateManager()  
    var isLoggedIn: Bool = false  
  
    private init() {}  
}

Usage:

AppStateManager.shared.isLoggedIn = true

KVO (Key-Value Observing)

KVO allows objects to observe changes in other objects’ properties.

Example:

class User: NSObject {  
    @objc dynamic var name: String  
    init(name: String) {  
        self.name = name  
    }  
}  
 
// In your ViewController  
var user = User(name: "John")  
var observation: NSKeyValueObservation?  
  
observation = user.observe(\.name, options: [.new]) { object, change in  
    print("User's name is now \(change.newValue ?? "")")  
}

Usage:

user.name = "Jane" // This will trigger the observation closure

Notification with NotificationCenter

Notifications are a way to broadcast and listen for changes from anywhere in the app.

Example:

// Define a notification name  
extension Notification.Name {  
    static let didUpdateProfile = Notification.Name("didUpdateProfile")  
}  
  
// Post a notification when the profile updates  
NotificationCenter.default.post(name: .didUpdateProfile, object: nil)  
  
// Add observer in viewDidLoad  
NotificationCenter.default.addObserver(self, selector: #selector(profileDidUpdate), name: .didUpdateProfile, object: nil)  
  
@objc func profileDidUpdate(notification: NSNotification) {  
    // Handle the profile update  
}

Usage:

NotificationCenter.default.post(name: .didUpdateProfile, object: nil)

Closure

Closures are self-contained blocks of functionality that can be passed and used throughout your code.

Example:

class DataLoader {  
    func loadData(completion: (Data?, Error?) -> Void) {  
        // Load data and return it via the completion  
        completion(Data(), nil)  
    }  
}  
  
class DataViewController: UIViewController {  
    var dataLoader = DataLoader()  
  
    func fetchData() {  
        dataLoader.loadData { [weak self] data, error in  
            // Make sure to capture 'self' weakly if it is used within the closure  
            self?.handle(data: data, error: error)  
        }  
    }  
      
    func handle(data: Data?, error: Error?) {  
        // Update UI accordingly  
    }  
}

Usage:

let dataVC = DataViewController()  
dataVC.fetchData()

Property Wrappers in SwiftUI

SwiftUI provides property wrappers such as @State, @Binding, and @ObservedObject to manage the state in a declarative way.

Example:

struct ContentView: View {  
    @State private var isSwitchedOn = false  
  
    var body: some View {  
        Toggle(isOn: $isSwitchedOn) {  
            Text("Switch")  
        }  
    }  
}

Usage:

// SwiftUI takes care of the usage through the user interface directly.

Combine Framework

Combine helps you manage the state by binding the user interface to your data model using publishers and subscribers.

class LoginFormViewModel: ObservableObject {  
    @Published var username: String = ""  
    @Published var password: String = ""  
      
    // Further Combine code to handle form submission and validation  
}

Usage:

let loginVM = LoginFormViewModel()  
loginVM.username = "SwiftUser"  
loginVM.password = "CombineRocks!"

Conclusion:

Each state management technique in Swift has its unique advantages and use cases. From the simplicity of MVC to the reactivity of Combine, you have the flexibility to choose the right tool for the job at the right time. Understanding these different approaches will arm you with the knowledge to create more maintainable, scalable, and robust iOS applications.


I hope you found this article enjoyable! If you appreciated the information provided, you have the option to support me by Buying Me A Coffee! Your gesture would be greatly appreciated!

Thank you for taking the time to read this article.

If you’re interested in more tech-related content, be sure to visit my website Digital Dive Hub for the latest blogs and updates.