From bcf2a2f0266c3363256f921d09f24478615384fa Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Mon, 22 Jul 2024 22:48:00 -0700 Subject: [PATCH] Improve compose reply view avatar scrolling animation --- .../ComposeUI/TextViewCaretScrolling.swift | 1 + .../ComposeUI/Views/ReplyStatusView.swift | 52 ++++++++++++++++--- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/Packages/ComposeUI/Sources/ComposeUI/TextViewCaretScrolling.swift b/Packages/ComposeUI/Sources/ComposeUI/TextViewCaretScrolling.swift index aa38ce18..a1f81f7d 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/TextViewCaretScrolling.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/TextViewCaretScrolling.swift @@ -39,6 +39,7 @@ extension TextViewCaretScrolling { let animator = UIViewPropertyAnimator(duration: 0.2, curve: .easeInOut) { scrollView.scrollRectToVisible(rectToMakeVisible, animated: false) + scrollView.layoutIfNeeded() } self.caretScrollPositionAnimator = animator animator.startAnimation() diff --git a/Packages/ComposeUI/Sources/ComposeUI/Views/ReplyStatusView.swift b/Packages/ComposeUI/Sources/ComposeUI/Views/ReplyStatusView.swift index 91706952..c6198897 100644 --- a/Packages/ComposeUI/Sources/ComposeUI/Views/ReplyStatusView.swift +++ b/Packages/ComposeUI/Sources/ComposeUI/Views/ReplyStatusView.swift @@ -76,13 +76,15 @@ struct ReplyStatusView: View { // once you scroll past the in-reply-to-content, the bottom of the avatar should be pinned to the bottom of the content offset = min(offset, maxOffset) - return AvatarImageView( - url: status.account.avatar, - size: 50, - style: controller.config.avatarStyle, - fetchAvatar: controller.fetchAvatar - ) - .offset(x: 0, y: offset) + return AvatarContainerRepresentable(offset: offset) { + AvatarImageView( + url: status.account.avatar, + size: 50, + style: controller.config.avatarStyle, + fetchAvatar: controller.fetchAvatar + ) + } + .frame(width: 50, height: 50) .accessibilityHidden(true) } @@ -94,3 +96,39 @@ private struct DisplayNameHeightPrefKey: PreferenceKey { value = nextValue() } } + +// This whole dance is necessary so that the offset can be animatable from +// UIKit animations, like TextViewCaretScrolling. +private struct AvatarContainerRepresentable: UIViewControllerRepresentable { + let offset: CGFloat + @ViewBuilder let content: Content + + func makeUIViewController(context: Context) -> Controller { + Controller(host: UIHostingController(rootView: content)) + } + + func updateUIViewController(_ uiViewController: Controller, context: Context) { + uiViewController.host.rootView = content + uiViewController.host.view.transform = CGAffineTransform(translationX: 0, y: offset) + } + + // This extra layer is necessary because applying a transform to the + // representable's VC's view doesn't seem to have an effect. + class Controller: UIViewController { + let host: UIHostingController + + init(host: UIHostingController) { + self.host = host + super.init(nibName: nil, bundle: nil) + addChild(host) + host.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] + view.addSubview(host.view) + host.view.frame = view.bounds + host.didMove(toParent: self) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + } +}