Skip to content

Instantly share code, notes, and snippets.

@clayellis
Last active February 4, 2023 22:52
Show Gist options
  • Save clayellis/4a12828bc90599c331a320f77cf104ed to your computer and use it in GitHub Desktop.
Save clayellis/4a12828bc90599c331a320f77cf104ed to your computer and use it in GitHub Desktop.
// This gist demonstrates how ObservableObjects can be composed in a simple way by leveraging a new
// @Republished property wrapper. @Republished republishes child state objectWillChange messages on its
// parent's objectWillChange publisher, tying the two together.
// The example app is from PointFree Co's episode: https://www.pointfree.co/episodes/ep146-derived-behavior-the-problem
// The one major change besides @Republished is that the "favorites" state has been extracted from the
// Counter/ProfileViewModels into a single source of truth: FavoritesViewModel.
import Combine
import SwiftUI
// MARK: - App
@main
struct DerivedBehaviorRepublishedApp: App {
@ObservedObject var appViewModel: AppViewModel
init() {
let favorites = FavoritesViewModel()
appViewModel = AppViewModel(
counter: CounterViewModel(favorites: favorites),
profile: ProfileViewModel(favorites: favorites)
)
}
var body: some Scene {
WindowGroup {
VanillaContentView(viewModel: appViewModel)
}
}
}
// MARK: - View Models
class AppViewModel: ObservableObject {
@Republished var counter: CounterViewModel
@Republished var profile: ProfileViewModel
init(counter: CounterViewModel, profile: ProfileViewModel) {
self.counter = counter
self.profile = profile
}
init(favorites: FavoritesViewModel) {
self.counter = CounterViewModel(favorites: favorites)
self.profile = ProfileViewModel(favorites: favorites)
}
}
class FavoritesViewModel: ObservableObject {
@Published var favorites: Set<Int> = []
}
class CounterViewModel: ObservableObject {
@Published var count = 0
@Republished var favorites: FavoritesViewModel
init(favorites: FavoritesViewModel) {
self.favorites = favorites
}
}
class ProfileViewModel: ObservableObject {
@Republished var favorites: FavoritesViewModel
init(favorites: FavoritesViewModel) {
self.favorites = favorites
}
}
// MARK: - Views
struct VanillaContentView: View {
@ObservedObject var viewModel: AppViewModel
var body: some View {
TabView {
VanillaCounterView(viewModel: self.viewModel.counter)
.tabItem { Text("Counter \(self.viewModel.counter.count)") }
VanillaProfileView(viewModel: self.viewModel.profile)
.tabItem { Text("Profile \(self.viewModel.profile.favorites.favorites.count)") }
}
}
}
struct VanillaCounterView: View {
@ObservedObject var viewModel: CounterViewModel
var body: some View {
VStack {
HStack {
Button("-") { self.viewModel.count -= 1 }
Text("\(self.viewModel.count)")
Button("+") { self.viewModel.count += 1 }
}
if self.viewModel.favorites.favorites.contains(self.viewModel.count) {
Button("Remove") {
self.viewModel.favorites.favorites.remove(self.viewModel.count)
}
} else {
Button("Save") {
self.viewModel.favorites.favorites.insert(self.viewModel.count)
}
}
}
}
}
struct VanillaProfileView: View {
@ObservedObject var viewModel: ProfileViewModel
var body: some View {
List {
ForEach(self.viewModel.favorites.favorites.sorted(), id: \.self) { number in
HStack {
Text("\(number)")
Spacer()
Button("Remove") {
self.viewModel.favorites.favorites.remove(number)
}
}
}
}
}
}
// MARK: - Republished
@propertyWrapper
struct Republished<Object: ObservableObject> where Object.ObjectWillChangePublisher == ObservableObjectPublisher {
static subscript<T: ObservableObject>(
_enclosingInstance instance: T,
wrapped wrappedKeyPath: ReferenceWritableKeyPath<T, Object>,
storage storageKeyPath: ReferenceWritableKeyPath<T, Self>
) -> Object where T.ObjectWillChangePublisher: ObservableObjectPublisher {
get {
let value = instance[keyPath: storageKeyPath].wrappedValue
if instance[keyPath: storageKeyPath].subscription == nil {
instance[keyPath: storageKeyPath].subscription = value.republish(on: instance)
}
return value
}
set {
if instance[keyPath: storageKeyPath].wrappedValue !== newValue {
instance[keyPath: storageKeyPath].subscription = newValue.republish(on: instance)
}
instance[keyPath: storageKeyPath].wrappedValue = newValue
}
}
var wrappedValue: Object
private var subscription: AnyCancellable? = nil
init(wrappedValue: Object) {
self.wrappedValue = wrappedValue
}
}
extension ObservableObject where Self.ObjectWillChangePublisher: ObservableObjectPublisher {
func republish<Other: ObservableObject>(on other: Other) -> AnyCancellable where Other.ObjectWillChangePublisher: ObservableObjectPublisher {
objectWillChange.sink(receiveValue: { [weak other] in other?.objectWillChange.send() })
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment