feat(tabbar): add TabBarBottomAccessoryModule and enhance TabBarRootView
- Introduced `TabBarBottomAccessoryModule` to provide a new bottom accessory view for the tab bar. - Updated `TabBarRootView` to utilize a custom tab bar controller, improving the management of the tab bar's visibility and behavior. - Refactored `UIWindow` extension methods for better readability and structure. - Enhanced `expo-module.config.json` to include the new module and improved formatting for better clarity. These changes aim to enhance the tab bar functionality and improve the overall user interface experience in the mobile application. Signed-off-by: Innei <tukon479@gmail.com>
This commit is contained in:
parent
d1b790036c
commit
87ea8b7a20
|
|
@ -53,7 +53,7 @@
|
|||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>137</string>
|
||||
<string>138</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@
|
|||
"TabBarPortalModule",
|
||||
"EnhancePagerViewModule",
|
||||
"EnhancePageViewModule",
|
||||
"ItemPressableModule"
|
||||
"ItemPressableModule",
|
||||
"TabBarBottomAccessoryModule"
|
||||
]
|
||||
},
|
||||
"android": {
|
||||
|
|
|
|||
|
|
@ -9,105 +9,102 @@ import UIKit
|
|||
|
||||
extension UIWindow {
|
||||
|
||||
|
||||
static func findViewController<T: UIViewController>(ofType type: T.Type) -> T? {
|
||||
static func findViewController<T: UIViewController>(ofType type: T.Type) -> T? {
|
||||
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
if let rootVC = window.rootViewController {
|
||||
return findViewControllerInHierarchy(rootVC, ofType: type)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
static private func findViewControllerInHierarchy<T: UIViewController>(_ viewController: UIViewController, ofType type: T.Type) -> T? {
|
||||
|
||||
if let targetVC = viewController as? T {
|
||||
return targetVC
|
||||
}
|
||||
|
||||
|
||||
if let navController = viewController as? UINavigationController {
|
||||
if let visibleVC = navController.visibleViewController {
|
||||
return findViewControllerInHierarchy(visibleVC, ofType: type)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let tabController = viewController as? UITabBarController {
|
||||
if let selectedVC = tabController.selectedViewController {
|
||||
return findViewControllerInHierarchy(selectedVC, ofType: type)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for childVC in viewController.children {
|
||||
if let foundVC = findViewControllerInHierarchy(childVC, ofType: type) {
|
||||
return foundVC
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let presentedVC = viewController.presentedViewController {
|
||||
return findViewControllerInHierarchy(presentedVC, ofType: type)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public static func findRNSNavigationController() -> UINavigationController? {
|
||||
|
||||
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let rootViewController = window.rootViewController
|
||||
|
||||
if let navController = rootViewController as? UINavigationController,
|
||||
NSStringFromClass(type(of: navController)).contains("RNSNavigationController")
|
||||
{
|
||||
return navController
|
||||
}
|
||||
|
||||
return findRNSNavigationControllerInChildren(of: rootViewController)
|
||||
let window = windowScene.windows.first
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func findRNSNavigationControllerInChildren(of viewController: UIViewController?)
|
||||
-> UINavigationController?
|
||||
if let rootVC = window.rootViewController {
|
||||
return findViewControllerInHierarchy(rootVC, ofType: type)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static private func findViewControllerInHierarchy<T: UIViewController>(
|
||||
_ viewController: UIViewController, ofType type: T.Type
|
||||
) -> T? {
|
||||
|
||||
if let targetVC = viewController as? T {
|
||||
return targetVC
|
||||
}
|
||||
|
||||
if let navController = viewController as? UINavigationController {
|
||||
if let visibleVC = navController.visibleViewController {
|
||||
return findViewControllerInHierarchy(visibleVC, ofType: type)
|
||||
}
|
||||
}
|
||||
|
||||
if let tabController = viewController as? UITabBarController {
|
||||
if let selectedVC = tabController.selectedViewController {
|
||||
return findViewControllerInHierarchy(selectedVC, ofType: type)
|
||||
}
|
||||
}
|
||||
|
||||
for childVC in viewController.children {
|
||||
if let foundVC = findViewControllerInHierarchy(childVC, ofType: type) {
|
||||
return foundVC
|
||||
}
|
||||
}
|
||||
|
||||
if let presentedVC = viewController.presentedViewController {
|
||||
return findViewControllerInHierarchy(presentedVC, ofType: type)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
public static func findRNSNavigationController() -> UINavigationController? {
|
||||
|
||||
guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let rootViewController = window.rootViewController
|
||||
|
||||
if let navController = rootViewController as? UINavigationController,
|
||||
NSStringFromClass(type(of: navController)).contains("RNSNavigationController")
|
||||
{
|
||||
guard let viewController = viewController else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let presentedVC = viewController.presentedViewController {
|
||||
if let navController = presentedVC as? UINavigationController,
|
||||
NSStringFromClass(type(of: navController)).contains("RNSNavigationController")
|
||||
{
|
||||
return navController
|
||||
}
|
||||
|
||||
if let result = findRNSNavigationControllerInChildren(of: presentedVC) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
for childVC in viewController.children {
|
||||
if let navController = childVC as? UINavigationController,
|
||||
NSStringFromClass(type(of: navController)).contains("RNSNavigationController")
|
||||
{
|
||||
return navController
|
||||
}
|
||||
|
||||
if let result = findRNSNavigationControllerInChildren(of: childVC) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return navController
|
||||
}
|
||||
|
||||
return findRNSNavigationControllerInChildren(of: rootViewController)
|
||||
}
|
||||
|
||||
private static func findRNSNavigationControllerInChildren(of viewController: UIViewController?)
|
||||
-> UINavigationController?
|
||||
{
|
||||
guard let viewController = viewController else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if let presentedVC = viewController.presentedViewController {
|
||||
if let navController = presentedVC as? UINavigationController,
|
||||
NSStringFromClass(type(of: navController)).contains("RNSNavigationController")
|
||||
{
|
||||
return navController
|
||||
}
|
||||
|
||||
if let result = findRNSNavigationControllerInChildren(of: presentedVC) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
for childVC in viewController.children {
|
||||
if let navController = childVC as? UINavigationController,
|
||||
NSStringFromClass(type(of: navController)).contains("RNSNavigationController")
|
||||
{
|
||||
return navController
|
||||
}
|
||||
|
||||
if let result = findRNSNavigationControllerInChildren(of: childVC) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
//
|
||||
// TabBarBottomAccessoryModule.swift
|
||||
// FollowNative
|
||||
//
|
||||
// Created by Innei on 2025-09-25
|
||||
//
|
||||
|
||||
import ExpoModulesCore
|
||||
import UIKit
|
||||
|
||||
public class TabBarBottomAccessoryModule: Module {
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("TabBarBottomAccessory")
|
||||
|
||||
View(TabBarBottomAccessoryView.self) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TabBarBottomAccessoryView: ExpoView {
|
||||
private weak var attachedRoot: TabBarRootView?
|
||||
|
||||
required init(appContext: AppContext? = nil) {
|
||||
super.init(appContext: appContext)
|
||||
}
|
||||
|
||||
deinit {
|
||||
detachFromRoot()
|
||||
}
|
||||
|
||||
override func didMoveToWindow() {
|
||||
super.didMoveToWindow()
|
||||
attachToNearestTabBarRoot()
|
||||
}
|
||||
|
||||
override func willMove(toWindow newWindow: UIWindow?) {
|
||||
if newWindow == nil {
|
||||
detachFromRoot()
|
||||
}
|
||||
super.willMove(toWindow: newWindow)
|
||||
}
|
||||
|
||||
#if RCT_NEW_ARCH_ENABLED
|
||||
|
||||
override func mountChildComponentView(_ childComponentView: UIView, index: Int) {
|
||||
attachToNearestTabBarRoot()
|
||||
}
|
||||
|
||||
override func unmountChildComponentView(_ childComponentView: UIView, index: Int) {
|
||||
detachFromRoot()
|
||||
|
||||
}
|
||||
#endif
|
||||
|
||||
func attachToNearestTabBarRoot() {
|
||||
if #available(iOS 26, *) {
|
||||
guard window != nil else { return }
|
||||
|
||||
CustomTabbarController.tabBarController.bottomAccessory = .init(contentView: self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func detachFromRoot() {
|
||||
if #available(iOS 26, *) {
|
||||
guard window != nil else { return }
|
||||
CustomTabbarController.tabBarController.bottomAccessory = nil
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -10,8 +10,9 @@ import Foundation
|
|||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
class TabBarRootView: ExpoView {
|
||||
private lazy var tabBarController = {
|
||||
@MainActor
|
||||
enum CustomTabbarController {
|
||||
static var tabBarController = {
|
||||
let tabBarController = UITabBarController()
|
||||
if #available(iOS 16.0, *), UIDevice.current.userInterfaceIdiom == .pad {
|
||||
tabBarController.tabBar.isTranslucent = false
|
||||
|
|
@ -27,19 +28,27 @@ class TabBarRootView: ExpoView {
|
|||
if #available(iOS 26.0, *) {
|
||||
tabBarController.isTabBarHidden = false
|
||||
tabBarController.tabBarMinimizeBehavior = .onScrollDown
|
||||
|
||||
}
|
||||
|
||||
tabBarController.tabBar.tintColor = Utils.accentColor
|
||||
|
||||
return tabBarController
|
||||
}()
|
||||
}
|
||||
|
||||
class TabBarRootView: ExpoView {
|
||||
private var tabBarController = CustomTabbarController.tabBarController
|
||||
|
||||
private let vc = UIViewController()
|
||||
private var tabViewControllers: [UIViewController] = []
|
||||
private var bottomAccessoryView: UIView?
|
||||
|
||||
private let onTabIndexChange = EventDispatcher()
|
||||
private let onTabItemPress = EventDispatcher()
|
||||
|
||||
|
||||
|
||||
required init(appContext: AppContext? = nil) {
|
||||
super.init(appContext: appContext)
|
||||
|
||||
|
|
@ -118,6 +127,7 @@ class TabBarRootView: ExpoView {
|
|||
let tabBarView = tabBarController.view!
|
||||
tabBarView.addSubview(tabBarPortalView)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override func willRemoveSubview(_ subview: UIView) {
|
||||
|
|
@ -129,6 +139,7 @@ class TabBarRootView: ExpoView {
|
|||
tabBarController.viewControllers = tabViewControllers
|
||||
tabBarController.didMove(toParent: vc)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,18 @@ import { isIos26 } from "@/src/lib/platform"
|
|||
export const ThemedBlurView = ({
|
||||
ref,
|
||||
tint,
|
||||
|
||||
tintColor,
|
||||
useGlass,
|
||||
...rest
|
||||
}: BlurViewProps & { ref?: React.Ref<BlurView | null>; useGlass?: boolean }) => {
|
||||
}: BlurViewProps & {
|
||||
ref?: React.Ref<BlurView | null>
|
||||
useGlass?: boolean
|
||||
/**
|
||||
* The tint color of the glass view, only works when `useGlass` is true
|
||||
*/
|
||||
tintColor?: string
|
||||
}) => {
|
||||
const { colorScheme } = useColorScheme()
|
||||
|
||||
const background = useColor("systemBackground")
|
||||
|
|
@ -26,15 +35,20 @@ export const ThemedBlurView = ({
|
|||
const useBlurView = Platform.OS === "ios" || "experimentalBlurMethod" in rest
|
||||
|
||||
if (isIos26 && useGlass) {
|
||||
return <GlassView style={rest.style} glassEffectStyle="regular" />
|
||||
return <GlassView style={rest.style} glassEffectStyle="regular" tintColor={tintColor} />
|
||||
}
|
||||
return useBlurView ? (
|
||||
<BlurView
|
||||
ref={ref}
|
||||
intensity={100}
|
||||
tint={colorScheme === "light" ? "systemChromeMaterialLight" : "systemChromeMaterialDark"}
|
||||
{...rest}
|
||||
/>
|
||||
<>
|
||||
<BlurView
|
||||
ref={ref}
|
||||
intensity={100}
|
||||
tint={colorScheme === "light" ? "systemChromeMaterialLight" : "systemChromeMaterialDark"}
|
||||
{...rest}
|
||||
/>
|
||||
{tintColor && (
|
||||
<View style={[StyleSheet.absoluteFillObject, { backgroundColor: tintColor }]} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<View
|
||||
ref={ref as any}
|
||||
|
|
@ -42,7 +56,7 @@ export const ThemedBlurView = ({
|
|||
style={StyleSheet.flatten([
|
||||
rest.style,
|
||||
{
|
||||
backgroundColor: background,
|
||||
backgroundColor: tintColor ?? background,
|
||||
opacity: 1,
|
||||
},
|
||||
])}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ const NativeTabBarHolder = () => {
|
|||
<Portal>
|
||||
<BottomTabContext value={bottomTabContext}>
|
||||
<NavigationInstanceContext value={navigation}>
|
||||
<View className="bg-red absolute inset-x-0 bottom-[68px] mb-4">
|
||||
<View className="absolute inset-x-0 bottom-[68px] mb-4">
|
||||
<GlassPlayerTabBar className="absolute inset-x-0 bottom-0" />
|
||||
</View>
|
||||
</NavigationInstanceContext>
|
||||
|
|
|
|||
|
|
@ -40,5 +40,5 @@ export function getDefaultHeaderHeight({
|
|||
headerHeight = 64
|
||||
}
|
||||
|
||||
return headerHeight + statusBarHeight
|
||||
return headerHeight + (modalPresentation ? 0 : statusBarHeight)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import type { ViewProps } from "react-native"
|
|||
import type { TabBarRootWrapperProps } from "./types"
|
||||
|
||||
export const TabBarPortalWrapper = requireNativeView<ViewProps>("TabBarPortal")
|
||||
export const TabBarBottomAccessoryWrapper = requireNativeView<ViewProps>("TabBarBottomAccessory")
|
||||
|
||||
export type TabScreenNativeProps = ViewProps & { title?: string }
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import type { IconNativeValues } from "@/src/constants/native-images"
|
|||
import type { TabbarIconProps, TabBarRootWrapperProps } from "./types"
|
||||
|
||||
export { View as TabBarPortalWrapper } from "react-native"
|
||||
export { View as TabBarBottomAccessoryWrapper } from "react-native"
|
||||
export type TabScreenNativeProps = React.ComponentProps<typeof View> & {
|
||||
title?: string
|
||||
icon?: FC<TabbarIconProps> | IconNativeValues
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ import { accentColor, useColor } from "@/src/theme/colors"
|
|||
import { MarkAllAsReadDialog } from "../dialogs/MarkAllAsReadDialog"
|
||||
|
||||
export const ActionGroup = ({ children, className }: PropsWithChildren<{ className?: string }>) => {
|
||||
return <View className={cn("flex flex-row items-center gap-2", className)}>{children}</View>
|
||||
return <View className={cn("flex flex-row items-center gap-1", className)}>{children}</View>
|
||||
}
|
||||
|
||||
export function HomeLeftAction() {
|
||||
|
|
@ -59,7 +59,7 @@ interface HeaderActionButtonProps {
|
|||
variant?: "primary" | "secondary"
|
||||
}
|
||||
|
||||
export const MarkAllAsReadActionButton = ({ variant = "primary" }: HeaderActionButtonProps) => {
|
||||
export const MarkAllAsReadActionButton = ({ variant = "secondary" }: HeaderActionButtonProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { size, color } = useButtonVariant({ variant })
|
||||
|
||||
|
|
@ -76,11 +76,11 @@ export const MarkAllAsReadActionButton = ({ variant = "primary" }: HeaderActionB
|
|||
|
||||
const useButtonVariant = ({ variant = "primary" }: HeaderActionButtonProps) => {
|
||||
const label = useColor("label")
|
||||
const size = 24
|
||||
const size = 20
|
||||
const color = variant === "primary" ? accentColor : label
|
||||
return { size, color }
|
||||
}
|
||||
export const UnreadOnlyActionButton = ({ variant = "primary" }: HeaderActionButtonProps) => {
|
||||
export const UnreadOnlyActionButton = ({ variant = "secondary" }: HeaderActionButtonProps) => {
|
||||
const { t } = useTranslation()
|
||||
const unreadOnly = useGeneralSettingKey("unreadOnly")
|
||||
const { size, color } = useButtonVariant({ variant })
|
||||
|
|
@ -110,7 +110,7 @@ export const UnreadOnlyActionButton = ({ variant = "primary" }: HeaderActionButt
|
|||
|
||||
export const FeedShareActionButton = ({
|
||||
feedId,
|
||||
variant = "primary",
|
||||
variant = "secondary",
|
||||
}: { feedId?: string } & HeaderActionButtonProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { size, color } = useButtonVariant({ variant })
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
import { FeedViewType } from "@follow/constants"
|
||||
import { useFeedById } from "@follow/store/feed/hooks"
|
||||
import { useIsSubscribed } from "@follow/store/subscription/hooks"
|
||||
import { isBizId } from "@follow/utils"
|
||||
import { isBizId, withOpacity } from "@follow/utils"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Pressable } from "react-native"
|
||||
import { Pressable, StyleSheet } from "react-native"
|
||||
import { RootSiblingParent } from "react-native-root-siblings"
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context"
|
||||
|
||||
import { ThemedBlurView } from "@/src/components/common/ThemedBlurView"
|
||||
import { BottomTabBarHeightContext } from "@/src/components/layouts/tabbar/contexts/BottomTabBarHeightContext"
|
||||
import { Text } from "@/src/components/ui/typography/Text"
|
||||
import { useNavigation } from "@/src/lib/navigation/hooks"
|
||||
|
|
@ -16,6 +17,7 @@ import { EntryListSelector } from "@/src/modules/entry-list/EntryListSelector"
|
|||
import { EntryListContext, useEntries, useSelectedView } from "@/src/modules/screen/atoms"
|
||||
import { TimelineHeader } from "@/src/modules/screen/TimelineSelectorProvider"
|
||||
import { FollowScreen } from "@/src/screens/(modal)/FollowScreen"
|
||||
import { accentColor } from "@/src/theme/colors"
|
||||
|
||||
export const FeedScreen: NavigationControllerView<{
|
||||
feedId: string
|
||||
|
|
@ -34,7 +36,7 @@ export const FeedScreen: NavigationControllerView<{
|
|||
<FeedScreenEntryList />
|
||||
{!isSubscribed && isBizId(feedIdentifier) && (
|
||||
<Pressable
|
||||
className="bg-accent absolute bottom-10 left-1/2 z-10 m-2 mx-auto -translate-x-1/2 rounded-full px-4 py-2"
|
||||
className="absolute bottom-10 left-1/2 z-10 m-2 mx-auto -translate-x-1/2 overflow-hidden rounded-full px-8 py-2"
|
||||
onPress={() => {
|
||||
navigation.presentControllerView(FollowScreen, {
|
||||
id: feedIdentifier,
|
||||
|
|
@ -42,6 +44,11 @@ export const FeedScreen: NavigationControllerView<{
|
|||
})
|
||||
}}
|
||||
>
|
||||
<ThemedBlurView
|
||||
useGlass
|
||||
style={StyleSheet.absoluteFillObject}
|
||||
tintColor={withOpacity(accentColor, 0.6)}
|
||||
/>
|
||||
<Text className="font-bold text-white">{t("words.follow")}</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Reference in New Issue