Skip to content

Commit

Permalink
Fix ObjC export of MultilinePillPickerView (#2122)
Browse files Browse the repository at this point in the history
  • Loading branch information
mischreiber authored Jan 29, 2025
1 parent 7ad34ed commit e93876b
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
//

import AppKit
import Combine
import SwiftUI

/// This is a work-in-progress control for hosting multiple rows of pill buttons. At present, this control
/// only supports a hard-coded two rows of elements.
@objc(MSFMultilinePillPickerView)
public final class MultilinePillPickerView: ControlHostingView {
public final class MultilinePillPickerView: ControlHostingView, ObservableObject {
/// Creates a multiline pill picker.
/// - Parameters:
/// - labels: An array of labels to show in the picker.
Expand All @@ -18,37 +19,48 @@ public final class MultilinePillPickerView: ControlHostingView {
@MainActor public init(labels: [String], action: (@MainActor (Int) -> Void)? = nil) {
self.labels = labels
self.action = action
let picker = MultilinePillPicker(labels: labels, action: action)
super.init(AnyView(picker))
super.init(AnyView(EmptyView()))

let wrapper = MultilinePillPickerWrapper(viewModel: viewModel)
self.hostingView.rootView = AnyView(wrapper)

// Set up observation to keep the view model in sync.
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 public var isEnabled: Bool = true {
didSet {
updatePicker()
}
}
@MainActor public var labels: [String] {
didSet {
updatePicker()
}
}
@MainActor public var action: (@MainActor (Int) -> Void)? {
didSet {
updatePicker()
}
}
@MainActor @Published public var isEnabled: Bool = true
@MainActor @Published public var labels: [String]
@MainActor @Published public var action: (@MainActor (Int) -> Void)?

@MainActor private let viewModel: MultilinePillPickerViewModel = .init()
}

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

/// Private wrapper `View` to map from view model to `MultilinePillPicker`.
fileprivate struct MultilinePillPickerWrapper: View {
@ObservedObject var viewModel: MultilinePillPickerViewModel

private func updatePicker() {
let picker = MultilinePillPicker(labels: labels, action: action)
.disabled(!isEnabled)
rootView = AnyView(picker)
var body: some View {
MultilinePillPicker(
labels: viewModel.labels,
action: viewModel.action
)
.disabled(!viewModel.isEnabled)
}
}
59 changes: 48 additions & 11 deletions Sources/FluentUI_macOS/Core/ControlHostingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,18 @@
//

import AppKit
import Combine
import SwiftUI

/// Common wrapper for hosting and exposing SwiftUI components to AppKit-based clients.
open class ControlHostingView: NSHostingView<AnyView> {
/// Common wrapper for hosting and exposing SwiftUI components to UIKit-based clients.
open class ControlHostingView: NSView {

/// The intrinsic content size of the wrapped SwiftUI view.
@objc public override var intrinsicContentSize: CGSize {
// Our desired size should always be the same as our hosted view.
return hostingView.intrinsicContentSize
}

/// Initializes and returns an instance of `ControlHostingContainer` that wraps `controlView`.
///
/// Unfortunately this class can't use Swift generics, which are incompatible with Objective-C interop. Instead we have to wrap
Expand All @@ -17,21 +25,50 @@ open class ControlHostingView: NSHostingView<AnyView> {
/// - Parameter safeAreaRegions: Passthrough to the respective property on NSHostingView.
/// Indicates which safe area regions the underlying hosting controller should add to its view.
public init(_ controlView: AnyView, safeAreaRegions: SafeAreaRegions = .all) {
super.init(rootView: controlView)
hostingView = NSHostingView.init(rootView: controlView)
if #available(macOS 13.3, *) {
self.sizingOptions = [.intrinsicContentSize]
self.safeAreaRegions = safeAreaRegions
hostingView.sizingOptions = [.intrinsicContentSize]
hostingView.safeAreaRegions = safeAreaRegions
}

layer?.backgroundColor = .clear
translatesAutoresizingMaskIntoConstraints = false
super.init(frame: .zero)

self.configureHostedView()
}

@MainActor @preconcurrency required public init?(coder: NSCoder) {
required public init?(coder: NSCoder) {
preconditionFailure("init(coder:) has not been implemented")
}

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

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() {
hostingView.layer?.backgroundColor = NSColor.clear.cgColor

addSubview(hostingView)
hostingView.translatesAutoresizingMaskIntoConstraints = false

let requiredConstraints = [
hostingView.leadingAnchor.constraint(equalTo: leadingAnchor),
hostingView.trailingAnchor.constraint(equalTo: trailingAnchor),
hostingView.bottomAnchor.constraint(equalTo: bottomAnchor),
hostingView.topAnchor.constraint(equalTo: topAnchor)
]
self.addConstraints(requiredConstraints)
}
}

0 comments on commit e93876b

Please sign in to comment.