Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ private struct ReactNativeViewWrapper: View {
bridge: bridge,
moduleName: moduleName,
initialProperties: initialProperties,
measuredHeight: fabricHeight,
onFabricHeightChange: { fabricHeight = $0 }
)
.frame(height: fabricHeight)
Expand All @@ -98,6 +99,7 @@ private struct ReactNativeRootView: UIViewRepresentable {
let bridge: RCTBridge?
let moduleName: String
let initialProperties: [String: Any]
let measuredHeight: CGFloat?
let onFabricHeightChange: (CGFloat) -> Void

func makeCoordinator() -> Coordinator {
Expand All @@ -122,14 +124,20 @@ private struct ReactNativeRootView: UIViewRepresentable {
}

func updateUIView(_ uiView: UIView, context: Context) {
// React Native updates the hosted view through its runtime.
// React Native updates the hosted view through its runtime. Refresh the
// callback when SwiftUI recreates this value around an existing UIView.
context.coordinator.onHeightChange = onFabricHeightChange
}

func sizeThatFits(_ proposal: ProposedViewSize, uiView: UIView, context: Context) -> CGSize? {
let proposedWidth = proposal.width ?? 0
let width = proposedWidth.isFinite && proposedWidth > 0
? proposedWidth
: UIScreen.main.bounds.width
if uiView is RCTSurfaceHostingProxyRootView,
let measuredHeight {
return CGSize(width: width, height: measuredHeight)
}
let maxHeight: CGFloat = uiView is RCTSurfaceHostingProxyRootView
? 10_000
: CGFloat.greatestFiniteMagnitude
Expand All @@ -147,7 +155,7 @@ private struct ReactNativeRootView: UIViewRepresentable {
private weak var rootView: RCTSurfaceHostingProxyRootView?
private var timer: Timer?
private var lastHeight: CGFloat = 0
private let onHeightChange: (CGFloat) -> Void
var onHeightChange: (CGFloat) -> Void

init(onHeightChange: @escaping (CGFloat) -> Void) {
self.onHeightChange = onHeightChange
Expand All @@ -156,9 +164,14 @@ private struct ReactNativeRootView: UIViewRepresentable {
func observe(_ rootView: RCTSurfaceHostingProxyRootView) {
self.rootView = rootView
rootView.delegate = self
timer = Timer.scheduledTimer(withTimeInterval: 0.2, repeats: true) { [weak self] _ in
// Fabric can change a mounted root component's frame without a
// corresponding intrinsic-size notification. Keep observing while
// this view is mounted, including while its parent is scrolling.
let timer = Timer(timeInterval: 0.2, repeats: true) { [weak self] _ in
self?.measure()
}
RunLoop.main.add(timer, forMode: .common)
self.timer = timer
}

func stopObserving() {
Expand All @@ -168,20 +181,37 @@ private struct ReactNativeRootView: UIViewRepresentable {
}

func rootViewDidChangeIntrinsicSize(_ rootView: RCTRootView) {
measure()
DispatchQueue.main.async { [weak self] in self?.measure() }
}

private func measure() {
guard let rootView, rootView.window != nil, rootView.bounds.width > 0 else { return }
let size = rootView.sizeThatFits(CGSize(width: rootView.bounds.width, height: 10_000))
guard size.height > 0, abs(size.height - lastHeight) > 0.5 else { return }
lastHeight = size.height
DispatchQueue.main.async { [onHeightChange] in
onHeightChange(size.height)
// Prefer the Fabric component's rendered frame. In the bridgeless
// path it can already be 160pt while the proxy and surface still
// report the 1pt fallback supplied by sizeThatFits below.
let contentHeight = FabricContentHeight.height(in: rootView.view)
let intrinsicHeight = rootView.intrinsicContentSize.height
let height: CGFloat
if let contentHeight, contentHeight > 1 || (lastHeight > 0 && contentHeight >= 0) {
height = contentHeight
} else if intrinsicHeight.isFinite, intrinsicHeight > 1 {
height = intrinsicHeight
} else {
let fittingHeight = rootView.sizeThatFits(
CGSize(width: rootView.bounds.width, height: 10_000)
).height
if fittingHeight.isFinite, fittingHeight > 1 {
height = fittingHeight
} else {
return // Never publish the temporary 1pt host as content.
}
}
guard abs(height - lastHeight) > 0.5 else { return }
lastHeight = height
DispatchQueue.main.async { [weak self] in
guard let self, self.rootView != nil else { return }
self.onHeightChange(height)
}
// Subsequent content size changes arrive through RCTRootViewDelegate.
timer?.invalidate()
timer = nil
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2024-present, salesforce.com, inc. All rights reserved.
*
* Reads the mounted Fabric root's content height. The surface hosting view can
* remain at its temporary 1-point size after its child has laid out, so its
* own sizeThatFits and intrinsicContentSize are not always useful to SwiftUI.
*/

import UIKit

enum FabricContentHeight {
static func height(in view: UIView) -> CGFloat? {
if NSStringFromClass(type(of: view)).hasSuffix("RCTRootComponentView") {
let height = view.bounds.height
return height.isFinite ? height : nil
}
for child in view.subviews {
if let height = height(in: child) {
return height
}
}
return nil
}
}
2 changes: 1 addition & 1 deletion AgentforceSDK-ReactNative-Bridge/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@salesforce/react-native-agentforce",
"version": "0.7.3-patch",
"version": "0.7.3-patch2",
"description": "Agentforce React Native bridge: JS API and native module for Service Agent and Employee Agent (auth bridge included; host app adds Mobile SDK).",
"main": "lib/index.js",
"types": "lib/index.d.ts",
Expand Down
30 changes: 30 additions & 0 deletions ios/Tests/FabricContentHeightTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import UIKit
import XCTest

private final class TestRCTRootComponentView: UIView {}

final class FabricContentHeightTests: XCTestCase {
func testFindsMountedComponentHeightBeyondOnePointHost() {
let surface = UIView(frame: CGRect(x: 0, y: 0, width: 334, height: 1))
let root = UIView(frame: surface.bounds)
let component = TestRCTRootComponentView(frame: CGRect(x: 0, y: 0, width: 334, height: 160))
surface.addSubview(root)
root.addSubview(component)

XCTAssertEqual(FabricContentHeight.height(in: surface), 160)

component.frame.size.height = 240
XCTAssertEqual(FabricContentHeight.height(in: surface), 240)

component.frame.size.height = 1
XCTAssertEqual(FabricContentHeight.height(in: surface), 1)

component.frame.size.height = 0
XCTAssertEqual(FabricContentHeight.height(in: surface), 0)
}

func testReturnsNilBeforeFabricComponentMounts() {
let surface = UIView(frame: CGRect(x: 0, y: 0, width: 334, height: 1))
XCTAssertNil(FabricContentHeight.height(in: surface))
}
}
1 change: 1 addition & 0 deletions ios/project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,4 @@ targets:
- path: Tests
type: group
- path: ../AgentforceSDK-ReactNative-Bridge/ios/Agentforce/Models/AgentMode.swift
- path: ../AgentforceSDK-ReactNative-Bridge/ios/Agentforce/Providers/FabricContentHeight.swift
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading