Skip to content

Commit

Permalink
Move to binding individual properties instead of the entire model at …
Browse files Browse the repository at this point in the history
…once
  • Loading branch information
mischreiber committed Jan 29, 2025
1 parent 910beef commit f1f4458
Show file tree
Hide file tree
Showing 2 changed files with 21 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
//

import AppKit
import Combine
import SwiftUI

/// This is a work-in-progress control for hosting multiple rows of pill buttons. At present, this control
Expand All @@ -25,43 +24,31 @@ public final class MultilinePillPickerView: ControlHostingView, ObservableObject
self.hostingView.rootView = AnyView(wrapper)

// Set up observation to keep the view model in sync.
viewModel.loadProperties(from: self)
cancellable = self.objectWillChange.sink { [weak self] in
DispatchQueue.main.async {
if let self {
self.viewModel.loadProperties(from: self)
}
}
}
bindProperty(from: self.$isEnabled, to: \.isEnabled, on: viewModel)
bindProperty(from: self.$labels, to: \.labels, on: viewModel)
bindProperty(from: self.$action, to: \.action, on: viewModel)
}

@MainActor required dynamic init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
preconditionFailure("init(coder:) has not been implemented")
}

@MainActor required init(rootView: AnyView) {
fatalError("init(rootView:) has not been implemented")
preconditionFailure("init(rootView:) has not been implemented")
}

@MainActor @Published public var isEnabled: Bool = true
@MainActor @Published public var labels: [String]
@MainActor @Published public var action: (@MainActor (Int) -> Void)?

@MainActor private var viewModel: MultilinePillPickerViewModel = .init()
private var cancellable: AnyCancellable?
}

/// Maps properties from `MultilinePillPickerView` to `MultilinePillPickerViewWrapper`.
fileprivate class MultilinePillPickerViewModel: ObservableObject {
@Published var isEnabled: Bool = true
@Published var labels: [String] = []
@Published var action: ((Int) -> Void)?

@MainActor func loadProperties(from host: MultilinePillPickerView) {
isEnabled = host.isEnabled
labels = host.labels
action = host.action
}
@Published var action: (@MainActor (Int) -> Void)?
}

/// Private wrapper `View` to map from view model to `MultilinePillPicker`.
Expand Down
15 changes: 15 additions & 0 deletions Sources/FluentUI_macOS/Core/ControlHostingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//

import AppKit
import Combine
import SwiftUI

/// Common wrapper for hosting and exposing SwiftUI components to UIKit-based clients.
Expand Down Expand Up @@ -40,6 +41,20 @@ open class ControlHostingView: NSView {
}

let hostingView: NSHostingView<AnyView>
var cancellables: Set<AnyCancellable> = []

// Helper function to facilitate binding ourselves to a ViewModel.
func bindProperty<Root: AnyObject, Value>(
from source: Published<Value>.Publisher,
to viewModelKeyPath: ReferenceWritableKeyPath<Root, Value>,
on viewModel: Root
) {
source
.sink { [weak viewModel] newValue in
viewModel?[keyPath: viewModelKeyPath] = newValue
}
.store(in: &cancellables)
}

/// Adds `hostingController.view` to ourselves as a subview, and enables necessary constraints.
private func configureHostedView() {
Expand Down

0 comments on commit f1f4458

Please sign in to comment.