Skip to content

Instantly share code, notes, and snippets.

@johndelong
Last active November 16, 2021 21:58
Show Gist options
  • Save johndelong/84ff7cb75f4f9a557baae872b70aa560 to your computer and use it in GitHub Desktop.
Save johndelong/84ff7cb75f4f9a557baae872b70aa560 to your computer and use it in GitHub Desktop.
General notes on the core components of SwiftUI

@ObservedObject / @Published

The ObservableObject conformance allows instances of a class to be used inside views, so that when important changes happen the View will reload.

The @Published property wrapper tells SwiftUI that changes to a property should trigger View reloads.

class Contact: ObservableObject {
    @Published var name: String
    @Published var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }

    func haveBirthday() -> Int {
        age += 1
        return age
    }
}

let john = Contact(name: "John Appleseed", age: 24)
cancellable = john.objectWillChange
    .sink { _ in
        print("\(john.age) will change")
}
print(john.haveBirthday())
// Prints "24 will change"
// Prints "25"

Note: You should only use @ObservedObject with views that were passed in from elsewhere. You should not use this property wrapper to create the initial instance of an observable object – that’s what @StateObject is for.

By default an ObservableObject synthesizes an objectWillChange publisher that emits the changed value before any of its @Published properties changes. To send a change event manually, you call objectWillChange.send().

class UserAuthentication: ObservableObject {
    var username = "Taylor" {
        willSet {
            objectWillChange.send()
        }
    }
}

https://www.hackingwithswift.com/quick-start/swiftui/how-to-use-observedobject-to-manage-state-from-external-objects https://www.hackingwithswift.com/quick-start/swiftui/how-to-send-state-updates-manually-using-objectwillchange https://developer.apple.com/documentation/combine/observableobject

@Binding:

Use when a subview needs to mutate the value being passed in. Otherwise, property should always be a let

@State:

Use when a view needs to retain information between redraws

Environment: ⬇️

Environment is a way to pass arbitrary things to your subviews at arbitrary depth. Modifiers like .font() and .buttonStyle() set a value in the environment that gets read by those views.

Example:

VStack {
  Text("")
  HStack {
    Text("")
    Button(...) {}
  }
}
.font(Font.system(size: 16))
.buttonStyle(FancyButtonStyle())

An Environment is similar to @ObservedObject in that they both allow a parent View to communicate state downward to its children. The difference is that Environment is more decoupled from the View because when you define a new Environment type you need to supply a default value. That way, Views can depend on it regardless if one of its parents defines a value - which also enables arbitrary depth. With an @ObservableObject you would need to pass the binding to each View in the hierarchy that might use it.

Example:

// ===========================================================
// Setup
// ===========================================================

public struct FormValidationErrorsKey: EnvironmentKey {
  public static let defaultValue: [String: String] = [:]
}

extension EnvironmentValues {
  public var formValidationErrors: FormValidationErrorsKey.Value {
    get { self[FormValidationErrorsKey.self] }
    set { self[FormValidationErrorsKey.self] = newValue }
  }
}

// Get access to environment as a struct/class property
@Environment(\.formValidationErrors) var errors: [String: String]

// Set environment at parent level
SomeView
  .environment(\.formValidationErrors, viewModel.validationErrors)

Preferences: ⬆️

Preferences are how views can communicate things back up to their parent.

Preferences are similar to @Binding in that they both enable a subview to communicate upward to its parent. Preference also needs a default value defined so you can rely on its presence.

Example:

public struct SizeKey: PreferenceKey {
  public static let defaultValue: CGSize = .zero
  public static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
    value = nextValue()
  }
}

public extension View {
  func measureSize(_ f: @escaping (CGSize) -> ()) -> some View {
    overlay(GeometryReader { g in
      Color.clear.preference(key: SizeKey.self, value: g.size)
    }
    .onPreferenceChange(SizeKey.self, perform: f))
  }
}

Read here for more information on preferences: https://swiftwithmajid.com/2020/01/15/the-magic-of-view-preferences-in-swiftui/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment