Swift Architecture Best Practices

On iOS applications, architecture

🔗 Published on medium

Dicusses WWDC talks which are noted here.

The making of Ice Cubes, an open source, SwiftUI Mastodon client.

🔗 Published on medium

Organzation

Use a lot of self-contained swift packages.

The packages are split by domains and features. There is very little code in the app itself; everything is self-contained in its own package.

Architecture

MVVM

The main view hold the viewModel using a @StateObject and the viewModel is passed as an @ObservedObject or a simple let variable in the subviews where it’s needed.

EnvironmentObject

Heavy use of environment object (also covered by WWDC talks on dataflow)

WindowGroup {
 appView
   .applyTheme(theme)
    .environmentObject(appAccountsManager)
    .environmentObject(appAccountsManager.currentClient)
    .environmentObject(quickLook)
    .environmentObject(currentAccount)
    .environmentObject(currentInstance)
    .environmentObject(userPreferences)
    .environmentObject(theme)
    .environmentObject(watcher)
    .environmentObject(pushNotificationsService)
}

There is no downside to injecting any number of them. It’s all about which view is connected to which environment object and how many updates you’re making in them.

🚨

You have to be aware including @EnvironmentObject at the top of your view don’t have any performance cost by itself. But updating any @Published property within this environment object will trigger a view update if the view is connected to it. Even if you’re not directly observing or using this property within your view. So be conscious about that.

Navigation

Central app router with NavigationStack. Agree on RouterDestinations and SheetDestinations to avoid spontaneous transitions or segues.

public enum RouterDestinations: Hashable {
  case accountDetail(id: String)
  case accountDetailWithAccount(account: Account)
  case accountSettingsWithAccount(account: Account, appAccount: AppAccount)
  case statusDetail(id: String)
  case statusDetailWithStatus(status: Status)
  case remoteStatusDetail(url: URL)
  case conversationDetail(conversation: Conversation)
  case hashTag(tag: String, account: String?)
  case list(list: Models.List)
  case followers(id: String)
  case following(id: String)
  case favoritedBy(id: String)
  case rebloggedBy(id: String)
  case accountsList(accounts: [Account])
}

public enum SheetDestinations: Identifiable {
  case newStatusEditor(visibility: Models.Visibility)
  case editStatusEditor(status: Status)
  case replyToStatusEditor(status: Status)
  case quoteStatusEditor(status: Status)
  case mentionStatusEditor(account: Account, visibility: Models.Visibility)
  case listEdit(list: Models.List)
  case listAddAccount(account: Account)
  case addAccount
  case addRemoteLocalTimeline
  case statusEditHistory(status: String)
  case settings
  case accountPushNotficationsSettings
  case report(status: Status)
  case shareImage(image: UIImage, status: Status)
}

Have an ObervableObject that handles navigation.

@MainActor
public class RouterPath: ObservableObject {
  public var client: Client?
  public var urlHandler: ((URL) -> OpenURLAction.Result)?

  @Published public var path: [RouterDestinations] = []
  @Published public var presentedSheet: SheetDestinations?

  public init() {}

  public func navigate(to: RouterDestinations) {
    path.append(to)
  }
}

Append and inject the navigation handler to any NavigationStack. Bind the path to the Stacks’ path property.

struct NotificationsTab: View {
  @StateObject private var routerPath = RouterPath()

  var body: some View {
    NavigationStack(path: $routerPath.path) {
      NotificationsListView()
        .withAppRouter()
        .withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
    }
    .environmentObject(routerPath)
}

Extend View to evaluate Router- and SheetDestinations.

@MainActor
extension View {
  func withAppRouter() -> some View {
    navigationDestination(for: RouterDestinations.self) { destination in
      switch destination {
      case let .accountDetail(id):
        AccountDetailView(accountId: id)
      case let .accountDetailWithAccount(account):
        AccountDetailView(account: account)
      ....
      }
    }
  }
  
  func withSheetDestinations(sheetDestinations: Binding<SheetDestinations?>) -> some View {
    sheet(item: sheetDestinations) { destination in
      switch destination {
      case let .replyToStatusEditor(status):
        StatusEditorView(mode: .replyTo(status: status))
          .withEnvironments()
          ...
       }
  }
  
  func withEnvironments() -> some View {
    environmentObject(CurrentAccount.shared)
      .environmentObject(UserPreferences.shared)
      ...
  }
}

In the actual view navigate to views

  @EnvironmentObject private var routerPath: RouterPath
  ...
  AvatarView(url: notification.account.avatar)
  .contentShape(Rectangle())
  .onTapGesture {
    routerPath.navigate(to: .accountDetailWithAccount(account: notification.account))
  }

or present sheets

Button {
  routerPath.presentedSheet = .addAccount
} label: {
  Image(systemName: "person.badge.plus")
}

Introducing MVVM into your SwiftUI project

🔗 Published on hackingwithswift.com

How to Use the Coordinator Pattern in SwiftUI

🔗 Published on quickbirdstudios.com

SOLID Principles in Swift

🔗 Published on medium

  1. The Single-responsibility principle: “There should never be more than one reason for a class to change. In other words, every class should have only one responsibility”.

  2. The Open–closed principle: “Software entities … should be open for extension, but closed for modification.”

  3. The Liskov substitution principle: “Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.”

  4. The Interface segregation principle: “Clients should not be forced to depend upon interfaces that they do not use.”

  5. The Dependency inversion principle: “Depend upon abstractions, [not] concretions.”

Single-responsibility principle:

class NetworkManager {
    var userAPIHandler: UserAPIHandler?
    var parseDataHandler: ParseDataHandler?
    var saveDataToDBHandler: SaveDataToDBHandler?
    
    init(userAPIHandler: UserAPIHandler, parseDataHandler: ParseDataHandler, saveDataToDBHandler: SaveDataToDBHandler) {
        self.userAPIHandler = userAPIHandler
        self.parseDataHandler = parseDataHandler
        self.saveDataToDBHandler = saveDataToDBHandler
    }
    
    func handleAllActions() {
        guard let userAPIHandler else { return }
        guard let parseDataHandler else { return }
        guard let saveDataToDBHandler else { return }
        
        let userData = userAPIHandler.getUsers()
        let userArray = parseDataHandler.parseDataToJson(data: userData)
        saveDataToDBHandler.saveDataToDB(users: userArray)
    }
}

class UserAPIHandler {
    func getUsers() -> Data {
        //Send API request and wait for a response
    }
}

class ParseDataHandler {
    func parseDataToJson(data: Data) -> [String] {
        // parse the data and convert it to array
    }
}

class SaveDataToDBHandler {
    func saveDataToDB(users: [String]) {
        // save that array into CoreData...
    }
}

**Open-close principle**

Extensions, protocols and proper inheritance.

**Liskov substitution principle**

> when we inherit from a base class, the subclass should not modify the behaviour of the base class functions

**Interface Segregations**

> users should not depend on interfaces or functionality they don’t need. It advises against providing overly complex or hard-to-understand interfaces.

- Objects only expose the methods they really implement.

```swift
protocol Flyable {
    func fly()
}

protocol Swimmable {
    func swim()
}

protocol Feedable {
    func eat()
}

class Flamingo: Flyable, Swimmable, Feedable  {
    func eat() {
        print("I can eat")
    }
    
    func fly() {
        print("I can fly")
    }
    
    func swim() {
        print("I can swim")
    }
}

class Dogs: Feedable {
    func eat() {
        print("I can eat")
    }
}

Dependency inversion principle

high-level modules should not depend on low-level modules. You should depend on interfaces, not implementations.