Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix layout issues on iOS on the new architecture #199

Merged
merged 42 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
f91352f
Update codegen config
j-piasecki Feb 21, 2024
117ddfb
Update podspec to include cpp files
j-piasecki Feb 21, 2024
5f3d841
Update js files
j-piasecki Feb 21, 2024
4153a93
POC implementation
j-piasecki Feb 21, 2024
23c1f30
Pass decorator shadow node family in initial state
j-piasecki Feb 21, 2024
5bcc732
Move implementation
j-piasecki Feb 21, 2024
4981758
Don't scan the entire tree when looking for text inputs
j-piasecki Feb 21, 2024
357eb62
Always pass default text attributes
j-piasecki Feb 22, 2024
e4f7e77
Move shadow family registry to another file
j-piasecki Feb 22, 2024
0e1d642
Fix first render
j-piasecki Feb 22, 2024
fc83732
Change RN prefix to RCT
j-piasecki Feb 22, 2024
1262c80
Add comments and minor changes
j-piasecki Feb 22, 2024
7c1fe24
Merge branch 'main' into @jpiasecki/new-arch-ios
j-piasecki Feb 26, 2024
acd43a3
Fix types for 0.73
j-piasecki Feb 26, 2024
a3da02b
Fix layout on reload
j-piasecki Feb 26, 2024
1b93823
Update ios/RCTLiveMarkdownModule.mm
j-piasecki Feb 27, 2024
47de394
Update ios/MarkdownCommitHook.mm
j-piasecki Feb 27, 2024
d79c992
Update ios/MarkdownTextInputDecoratorState.h
j-piasecki Feb 27, 2024
33b3ab6
Update ios/MarkdownCommitHook.h
j-piasecki Feb 27, 2024
34b5062
Update ios/MarkdownCommitHook.mm
j-piasecki Feb 27, 2024
916c5d6
Update ios/MarkdownTextInputDecoratorView.mm
j-piasecki Feb 27, 2024
e2701ff
Update ios/MarkdownCommitHook.h
j-piasecki Feb 27, 2024
3e6e257
Update ios/MarkdownCommitHook.mm
j-piasecki Feb 27, 2024
54dcad2
Sort imports
j-piasecki Feb 27, 2024
48b0b27
Fix indent
j-piasecki Feb 27, 2024
4e4f305
Move cast, add some const refs
j-piasecki Feb 27, 2024
ae0d8ba
Rename fields
j-piasecki Feb 27, 2024
2a423ff
Rename module, drop prefix
j-piasecki Feb 27, 2024
e78ee4b
Merge remote-tracking branch 'fork/@jpiasecki/new-arch-ios' into @jpi…
j-piasecki Feb 27, 2024
7dd9fc0
Merge branch 'main' into @jpiasecki/new-arch-ios
j-piasecki Feb 29, 2024
6fd508c
Merge branch 'main' into @jpiasecki/new-arch-ios
j-piasecki Mar 28, 2024
8fcaa4e
Dont use UI APIs on js thread
j-piasecki Mar 28, 2024
bd15f8a
Merge branch 'main' into @jpiasecki/new-arch-ios
j-piasecki Apr 4, 2024
17e4c1e
Update styles on props change
j-piasecki Apr 8, 2024
dcab103
Revert new-arch flags and deps
j-piasecki Apr 9, 2024
372e41e
Exclude new-arch code on the old-arch
j-piasecki Apr 9, 2024
7bf1f3c
Format code
j-piasecki Apr 9, 2024
ce9ffa5
Use correct cocoapods version
j-piasecki Apr 9, 2024
b5e3558
Merge branch 'main' into @jpiasecki/new-arch-ios
j-piasecki Apr 9, 2024
1ef6f88
Fix new arch compilation after main merge
j-piasecki Apr 9, 2024
89408c2
Merge branch 'main' into @jpiasecki/new-arch-ios
j-piasecki Apr 9, 2024
6f11c78
Make sure it works with frameworks
j-piasecki Apr 9, 2024
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
9 changes: 8 additions & 1 deletion RNLiveMarkdown.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,16 @@ Pod::Spec.new do |s|
s.platforms = { :ios => "11.0" }
s.source = { :git => "https://github.com/expensify/react-native-live-markdown.git", :tag => "#{s.version}" }

s.source_files = "ios/**/*.{h,m,mm}"
s.source_files = "ios/**/*.{h,m,mm,cpp}"

s.resources = "parser/react-native-live-markdown-parser.js"

install_modules_dependencies(s)

if ENV['USE_FRAMEWORKS'] && ENV['RCT_NEW_ARCH_ENABLED']
add_dependency(s, "React-Fabric", :additional_framework_paths => [
"react/renderer/textlayoutmanager/platform/ios",
"react/renderer/components/textinput/iostextinput",
])
end
end
12 changes: 10 additions & 2 deletions example/ios/LiveMarkdownExample.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -572,13 +572,17 @@
);
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
OTHER_CFLAGS = "$(inherited)";
OTHER_CFLAGS = (
"$(inherited)",
" ",
);
OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)",
"-DFOLLY_NO_CONFIG",
"-DFOLLY_MOBILE=1",
"-DFOLLY_USE_LIBCPP=1",
"-DFOLLY_CFG_NO_COROUTINES=1",
" ",
);
OTHER_LDFLAGS = (
"$(inherited)",
Expand Down Expand Up @@ -643,13 +647,17 @@
"\"$(inherited)\"",
);
MTL_ENABLE_DEBUG_INFO = NO;
OTHER_CFLAGS = "$(inherited)";
OTHER_CFLAGS = (
"$(inherited)",
" ",
);
OTHER_CPLUSPLUSFLAGS = (
"$(OTHER_CFLAGS)",
"-DFOLLY_NO_CONFIG",
"-DFOLLY_MOBILE=1",
"-DFOLLY_USE_LIBCPP=1",
"-DFOLLY_CFG_NO_COROUTINES=1",
" ",
);
OTHER_LDFLAGS = (
"$(inherited)",
Expand Down
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1111,7 +1111,7 @@ PODS:
- React-jsi (= 0.73.4)
- React-logger (= 0.73.4)
- React-perflogger (= 0.73.4)
- RNLiveMarkdown (0.1.43):
- RNLiveMarkdown (0.1.44):
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core
Expand Down Expand Up @@ -1371,7 +1371,7 @@ SPEC CHECKSUMS:
React-runtimescheduler: ed48e5faac6751e66ee1261c4bd01643b436f112
React-utils: 6e5ad394416482ae21831050928ae27348f83487
ReactCommon: 840a955d37b7f3358554d819446bffcf624b2522
RNLiveMarkdown: a63967738fd835165f740453942c193275d85936
RNLiveMarkdown: b0fe5fbcfa24b0dc190b6d246793d528a1c7c452
SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17
Yoga: 64cd2a583ead952b0315d5135bf39e053ae9be70

Expand Down
42 changes: 42 additions & 0 deletions ios/MarkdownCommitHook.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#pragma once
#ifdef RCT_NEW_ARCH_ENABLED

#include <react/renderer/components/iostextinput/TextInputShadowNode.h>
#include <react/renderer/uimanager/UIManager.h>
#include <react/renderer/uimanager/UIManagerCommitHook.h>

#include <memory>

#include "MarkdownTextInputDecoratorShadowNode.h"

using namespace facebook::react;

namespace livemarkdown {

struct MarkdownTextInputDecoratorPair {
const std::shared_ptr<const TextInputShadowNode> textInput;
const std::shared_ptr<const MarkdownTextInputDecoratorShadowNode> decorator;
};

class MarkdownCommitHook : public UIManagerCommitHook {
public:
MarkdownCommitHook(const std::shared_ptr<UIManager> &uiManager);

~MarkdownCommitHook() noexcept override;

void commitHookWasRegistered(UIManager const &) noexcept override {}

void commitHookWasUnregistered(UIManager const &) noexcept override {}

RootShadowNode::Unshared shadowTreeWillCommit(
ShadowTree const &shadowTree,
RootShadowNode::Shared const &oldRootShadowNode,
RootShadowNode::Unshared const &newRootShadowNode) noexcept override;

private:
const std::shared_ptr<UIManager> uiManager_;
};

} // namespace livemarkdown

#endif // RCT_NEW_ARCH_ENABLED
195 changes: 195 additions & 0 deletions ios/MarkdownCommitHook.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
#ifdef RCT_NEW_ARCH_ENABLED

#include <React/RCTUtils.h>
#include <react/renderer/core/ComponentDescriptor.h>
#include <react/renderer/textlayoutmanager/RCTAttributedTextUtils.h>

#include "MarkdownCommitHook.h"
#include "MarkdownShadowFamilyRegistry.h"
#include "RCTMarkdownStyle.h"
#include "RCTMarkdownUtils.h"

using namespace facebook::react;

namespace livemarkdown {

MarkdownCommitHook::MarkdownCommitHook(
const std::shared_ptr<UIManager> &uiManager)
: uiManager_(uiManager) {
uiManager_->registerCommitHook(*this);
}

MarkdownCommitHook::~MarkdownCommitHook() noexcept {
uiManager_->unregisterCommitHook(*this);
}

RootShadowNode::Unshared MarkdownCommitHook::shadowTreeWillCommit(
ShadowTree const &, RootShadowNode::Shared const &,
RootShadowNode::Unshared const &newRootShadowNode) noexcept {
auto rootNode = newRootShadowNode->ShadowNode::clone(ShadowNodeFragment{});

// A preface to why we do the weird thing below:
// On the new architecture there are two ways of measuring text on iOS: by
// value and by pointer. When done by value, the attributed string to be
// measured is created on the c++ side. We cannot modify this process as we do
// not extend TextInputShadowNode. We also cannot really change the layout
// manager used to do this, since it's a private field (ok, we can but in a
// not very nice way). But also, the logic for parsing and applying markdown
// is written in JS/ObjC and we really wouldn't want to reimplement it in c++.
//
// Nice thing is that it can also be done by pointer to NSAttributedString,
// which is the platform's way to handle styled text, and is also used by Live
// Markdown. On this path, the measurement is done by the OS APIs. The thing
// we want to make sure of, is that markdown-decorated text input always uses
// this path and uses a pointer to a string with markdown styles applied.
// Thankfully, RN provides nice utility functions that allow to convert
// between the RN's AttributedString and iOS's NSAttributedString. The logic
// below does exactly that.

// In order to properly apply markdown formatting to the text input, we need
// to update the TextInputShadowNode's state with styled string, but we only
// have access to the ShadowNodeFamilies of the decorator components. We also
// know that a markdown decorator is always preceded with the TextInput to
// decorate, so we need to take the sibling.
std::vector<MarkdownTextInputDecoratorPair> nodesToUpdate;
MarkdownShadowFamilyRegistry::runForEveryFamily([&rootNode, &nodesToUpdate](
ShadowNodeFamily::Shared
family) {
// get the path from the root to the node from the decorator family
const auto ancestors = family->getAncestors(*rootNode);

if (!ancestors.empty()) {
auto &parentNode = ancestors.back().first.get();
auto index = ancestors.back().second;

// this is node represented by one of the registered families and since we
// only register markdown decorator shadow families, static casting should
// be safe here
const auto &decoratorNode =
std::static_pointer_cast<const MarkdownTextInputDecoratorShadowNode>(
parentNode.getChildren().at(index));
// text input always precedes the decorator component
const auto &previousSibling = parentNode.getChildren().at(index - 1);

if (const auto &textInputNode =
std::dynamic_pointer_cast<const TextInputShadowNode>(
previousSibling)) {
// store the pair of text input and decorator to update in the next step
// we need both, decorator to get markdown style and text input to
// update it
nodesToUpdate.push_back({
textInputNode,
decoratorNode,
});
}
}
});

for (const auto &nodes : nodesToUpdate) {
const auto &textInputState =
*std::static_pointer_cast<const ConcreteState<TextInputState>>(
nodes.textInput->getState());
const auto &stateData = textInputState.getData();
const auto fontSizeMultiplier =
newRootShadowNode->getConcreteProps().layoutContext.fontSizeMultiplier;

// We only want to update the shadow node when the attributed string is
// stored by value If it's stored by pointer, the markdown formatting should
// already by applied to it, since the only source of that pointer (besides
// this commit hook) is RCTTextInputComponentView, which has the relevant
// method swizzled to make sure the markdown styles are always applied
// before updating state. There are two caveats:
// 1. On the first render the swizzled method will not apply markdown since
// the native component
// is not mounted yet. In that case we save the tag to update in the
// method applying markdown formatting and apply it here instead,
// preventing wrong layout on reloads.
// 2. When the markdown style prop is changed, the native state needs to be
// updated to reflect
// them. In that case the relevant tag is saved in the registry when the
// new shadow node is created.
if (stateData.attributedStringBox.getMode() ==
AttributedStringBox::Mode::Value ||
MarkdownShadowFamilyRegistry::shouldForceUpdate(
nodes.textInput->getTag())) {
rootNode = rootNode->cloneTree(
nodes.textInput->getFamily(),
[&nodes, &textInputState, &stateData,
fontSizeMultiplier](const ShadowNode &node) {
const auto &markdownProps = *std::static_pointer_cast<
MarkdownTextInputDecoratorViewProps const>(
nodes.decorator->getProps());
const auto &textInputProps =
*std::static_pointer_cast<TextInputProps const>(
nodes.textInput->getProps());

const auto defaultTextAttributes =
textInputProps.getEffectiveTextAttributes(fontSizeMultiplier);
const auto defaultNSTextAttributes =
RCTNSTextAttributesFromTextAttributes(defaultTextAttributes);

// this can possibly be optimized
RCTMarkdownStyle *markdownStyle = [[RCTMarkdownStyle alloc]
initWithStruct:markdownProps.markdownStyle];
RCTMarkdownUtils *utils = [[RCTMarkdownUtils alloc] init];
[utils setMarkdownStyle:markdownStyle];

// convert the attibuted string stored in state to
// NSAttributedString
auto nsAttributedString =
RCTNSAttributedStringFromAttributedStringBox(
stateData.attributedStringBox);

// Handles the first render, where the text stored in props is
// different than the one stored in state The one in state is empty,
// while the one in props is passed from JS If we don't update the
// state here, we'll end up with a one-default-line-sized text
// input. A better condition to do that can be probably chosen, but
// this seems to work
auto plainString =
std::string([[nsAttributedString string] UTF8String]);
if (plainString != textInputProps.text) {
// creates new AttributedString from props, adapted from
// TextInputShadowNode (ios one, text inputs are
// platform-specific)
auto attributedString = AttributedString{};
attributedString.appendFragment(AttributedString::Fragment{
textInputProps.text, defaultTextAttributes});

auto attachments = BaseTextShadowNode::Attachments{};
BaseTextShadowNode::buildAttributedString(
defaultTextAttributes, *nodes.textInput, attributedString,
attachments);

// convert the newly created attributed string to
// NSAttributedString
nsAttributedString = RCTNSAttributedStringFromAttributedStringBox(
AttributedStringBox{attributedString});
}

// apply markdown
auto newString = [utils parseMarkdown:nsAttributedString
withAttributes:defaultNSTextAttributes];

// create a clone of the old TextInputState and update the
// attributed string box to point to the string with markdown
// applied
auto newStateData = std::make_shared<TextInputState>(stateData);
newStateData->attributedStringBox =
RCTAttributedStringBoxFromNSAttributedString(newString);

// clone the text input with the new state
return node.clone({
.state = std::make_shared<const ConcreteState<TextInputState>>(
newStateData, textInputState),
});
});
}
}

return std::static_pointer_cast<RootShadowNode>(rootNode);
}

} // namespace livemarkdown

#endif // RCT_NEW_ARCH_ENABLED
58 changes: 58 additions & 0 deletions ios/MarkdownShadowFamilyRegistry.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#ifdef RCT_NEW_ARCH_ENABLED

#include "MarkdownShadowFamilyRegistry.h"

std::set<facebook::react::ShadowNodeFamily::Shared>
MarkdownShadowFamilyRegistry::familiesToUpdate_;
std::set<facebook::react::Tag> MarkdownShadowFamilyRegistry::forcedUpdates_;
std::mutex MarkdownShadowFamilyRegistry::mutex_;

void MarkdownShadowFamilyRegistry::registerFamilyForUpdates(
facebook::react::ShadowNodeFamily::Shared family) {
auto lock =
std::unique_lock<std::mutex>(MarkdownShadowFamilyRegistry::mutex_);
MarkdownShadowFamilyRegistry::familiesToUpdate_.insert(family);
}

void MarkdownShadowFamilyRegistry::unregisterFamilyForUpdates(
facebook::react::ShadowNodeFamily::Shared family) {
auto lock =
std::unique_lock<std::mutex>(MarkdownShadowFamilyRegistry::mutex_);
MarkdownShadowFamilyRegistry::familiesToUpdate_.erase(family);
}

void MarkdownShadowFamilyRegistry::reset() {
auto lock =
std::unique_lock<std::mutex>(MarkdownShadowFamilyRegistry::mutex_);
MarkdownShadowFamilyRegistry::familiesToUpdate_.clear();
MarkdownShadowFamilyRegistry::forcedUpdates_.clear();
}

void MarkdownShadowFamilyRegistry::runForEveryFamily(
std::function<void(facebook::react::ShadowNodeFamily::Shared)> fun) {
auto lock =
std::unique_lock<std::mutex>(MarkdownShadowFamilyRegistry::mutex_);
for (auto &family : MarkdownShadowFamilyRegistry::familiesToUpdate_) {
fun(family);
}
}

void MarkdownShadowFamilyRegistry::forceNextStateUpdate(
facebook::react::Tag tag) {
auto lock =
std::unique_lock<std::mutex>(MarkdownShadowFamilyRegistry::mutex_);
forcedUpdates_.insert(tag);
}

bool MarkdownShadowFamilyRegistry::shouldForceUpdate(facebook::react::Tag tag) {
auto lock =
std::unique_lock<std::mutex>(MarkdownShadowFamilyRegistry::mutex_);
bool force = forcedUpdates_.contains(tag);
if (force) {
forcedUpdates_.erase(tag);
return true;
}
return false;
}

#endif
Loading
Loading