SwiftUI研究:原生路由导航重构封装研究

一、思路来源

使用原生SwiftUI 导航时感觉特别难用,要在开始跳转的页面内返回被跳转页面。感觉代码耦合度更高了,就想尝试实现类似 flutter 中 GetX 的极简路由跳转方式,现在有了初步实现(还不完美),分享给大家。

设计思路:

  • 页面设计为 AppPage 持有 id,字符串路由,页面,参数等属性。使用之前注册到全局字典,可以根据路由在全局字典中查询页面。
  • 用一个路由管理单例类 Router 持有所有 NavigationPath,每次跳转页面时获取当前的 NavigationPath 然后根据路由查询设置参数进行跳转。

二、使用

Tab主页面
swift 复制代码
import SwiftUI

struct ContentView: View {

    @StateObject private var router = Router.shared

    @State private var showDrawer = false

  
    /// 隐藏 TabBar
    var hideTabBar: Visibility {
        let result = router.path.isEmpty
        return result ? .visible : .hidden
    }


    var body: some View {
        ZStack(alignment: .leading, content: {
            // 主界面内容
             buildTabView()
                .offset(x: showDrawer ? UIScreen.main.bounds.size.width * 0.75 : 0)
                .disabled(showDrawer) // 禁用主界面交互
                .animation(.easeInOut, value: showDrawer)
        })
    }

    func buildTabView() -> some View {
        return TabView(selection: $router.selectedTab) {
            NavigationStack(path: $router.path) {
                TabHomeView()
                    .navigationBarCustom(title: "首页", hideBack: true)
            }
            .toolbar(hideTabBar, for: .tabBar)
            .tabItem {
                Image(systemName: "house.fill")
                Text("首页")
            }
            .tag(0)

            NavigationStack(path: $router.path) {
                TabMessageView()
                    .navigationBarCustom(title: "消息", hideBack: true)
            }
            .toolbar(hideTabBar, for: .tabBar)
            .tabItem {
                Image(systemName: "message.fill")
                Text("发现")
            }
            .tag(1)

            NavigationStack(path: $router.path) {
                TabFindView()
                    .navigationBarCustom(title: "发现", hideBack: true)

            }
            .toolbar(hideTabBar, for: .tabBar)
            .tabItem {
                Image(systemName: "safari.fill")
                Text("发现")
            }
            .tag(2)

            NavigationStack(path: $router.path) {
                TabTestView()
                    .navigationBarCustom(title: "测试", hideBack: true)
            }

            .toolbar(hideTabBar, for: .tabBar)
            .tabItem {
                Image(systemName: "infinity.circle")
                Text("测试")
            }
            .tag(3)

            
            NavigationStack(path: $router.path) {
                TabProfileView()
                    .navigationBarCustom(title: "我的", hideBack: true)

            }
            .toolbar(hideTabBar, for: .tabBar)
            .tabItem {
                Image(systemName: "person.fill")
                Text("我的")
            }
            .tag(4)
        }
    }
}
第一个tab 的首页面
swift 复制代码
struct TabHomeView: View {
    var body: some View {
        return HomeView()
    }
}


// MARK: - RouterModel
struct RouterModel: Hashable {

    let name: String

    let route: String

    func hash(into hasher: inout Hasher) {
        hasher.combine(name)
        hasher.combine(route)
    }

    

    static func == (lhs: RouterModel, rhs: RouterModel) -> Bool {
        return lhs.name == rhs.name && lhs.route == rhs.route
    }
}

struct HomeView: View {

    @StateObject private var router = Router.shared

    var items = AppRouter.pages.map({ e in
        return RouterModel(name: e.0, route: e.0)
    });

    let data: [String] = ["Item 1", "Item 2", "Item 3", "Item 4"]
    let avatar: String = "https://yl-prescription-share.oss-cn-beijing.aliyuncs.com/test/message/document/1737078705/im/msg/rec/651722301611577344.jpg";

    var body: some View {
        NavigationStack(path: $router.path) {
            List {
                SystemItemsSection()
                CustomItemsSection()
                Button("Button") {
                    router.toNamed(AppRouter.detail)
                }
            }
            .listStyle(GroupedListStyle())
            .navigationTitle("\(clsName)")
            .navigationDestination(for: AppPage<AnyView>.self) { page in
                page.makeView()

            }
        }
    }

    // MARK: - Subviews
    private func SystemItemsSection() -> some View {
        Section(header: Text("页面").font(.headline)) {
            ForEach(items, id: \.self) { item in
                RouterItemView(item: item, avatar: avatar)
                    .onTapGesture {
                        DDLog("onTapGesture")
                        router.toNamed(item.route)
                    }
            }
        }
    }

   
    private func CustomItemsSection() -> some View {
        Section(header: Text("自定义").font(.headline)) {
            CustomOneCell(showArrow: false) {
                Text("CustomOneCell")
            } detail: {
                Text("Subtitle")
                    .font(.subheadline)
                    .foregroundColor(.gray)
            }
        }
    }
}

  
// MARK: - RouterItemView
struct RouterItemView: View {

    let item: RouterModel

    let avatar: String

    @StateObject private var router = Router.shared

    var body: some View {
        ListItemView(
            avatar: avatar,
            isTitleRightHide: true,
            isSubtitleRightHide: true,
            title: {
                Text(item.name)
            },
            titleRight: {
                Text("titleRight")
                    .font(.body)
            },
            subtitle: {
                Text("")
                    .font(.body)
            },
            subtitleRight: {
                Text("subtitleRight")
                    .font(.body)
            }
        )
        .onTapGesture {
            router.toNamed(item.route)
        }
    }
}
DetailView详情页
less 复制代码
struct DetailView: View {

    @StateObject private var router = Router.shared

//    init(path: NavigationPath = NavigationPath()) {
//        self.path = path
//    }

    var body: some View {
        NavigationStack(path: $router.path) {
            VStack(alignment: .leading, content: {
                Button("Router导航") {
                    let router = Router.shared
                    router.toNamed(AppRouter.detail)
                }

                Button( "SwiftUINavigator 导航") {
                    let navigator = SwiftUINavigator(path: $router.path)
                    navigator.push(AnyView(TabTestView()))
                }

                Button {
                    router.toNamed(AppRouter.detail)
                } label: {
                    Text("Button")
                }
            })
            .padding()
            .navigationBarCustom(title: "\(clsName)")
  //            .navigationDestination(for: AppPage<AnyView>.self) { page in
  //                page.makeView()
  //            }
           }
    }
}

三、路由源码

swift 复制代码
import SwiftUI

  
// 路由
@MainActor class AppRouter {

    static let home = "/"

    static let settings = "/settings"

    static let profile = "/profile"

    static let detail = "/detail"

    static let imageViewer = "/imageViewer"

    static let discovery = "/discovery"

    static let discoveryDetail = "/discovery/detail"

    static let collection = "/collection"

    static let notification = "/notification"

    static let editProfile = "/profile/edit"

    

    // 系统相关组件

    static let animatePage = "/animate"

    static let component = "/component"

    static let customeModifier = "/customeModifier"

    static let dynamicContent = "/dynamicContent"

    static let geometryReader = "/geometryReader"

    static let gesture = "/gesture"

    static let nav = "/nav"

    static let imageGalleryDemo = "/imageGallery"

    

    // 自定义组件

    static let wrap = "/wrap"

    static let circleLayout = "/circleLayout"

    static let pager = "/pager"

    static let unknow = "/unknow"

    static let custom = "/custom"

    static let test = "/test"

    static let pickerViewPage = "/pickerViewPage"

  


    // 第三方

    static let notificationBannerView = "/notificationBannerView"

    static let fileHelperDemo = "/FileHelperDemo"

    static let modelCodablePage = "/modelCodablePage"

    static let locationDemo = "/locationDemo"

    // 新增

    static let dataTypeDemo = "/dataTypeDemo"


    /// 路由

    static let pages: [(String, ([String: Any]) -> AnyView, ([String: Any]) -> String)] = [

        (AppRouter.home, { _ in AnyView(HomeView()) }, { _ in "首页" }),

        (AppRouter.settings, { _ in AnyView(SettingsView()) }, { _ in "设置" }),

        (AppRouter.profile, { _ in AnyView(ProfileView()) }, { _ in "个人中心" }),

        (AppRouter.detail, { args in AnyView(DetailView()) }, { args in args["title"] as? String ?? "详情" }),

        (AppRouter.imageViewer, { args in

            let images = args["images"] as? [String] ?? []

            let selectedIndex = args["selectedIndex"] as? Int ?? 0

            let isPresented = args["isPresented"] as? Bool ?? true

            return AnyView(NImagePreviewer(

                images: images,

                selectedIndex: selectedIndex,

                isPresented: .constant(isPresented)

            ))

        }, { args in

            let selectedIndex = args["selectedIndex"] as? Int ?? 0

            let images = args["images"] as? [String] ?? []

            return "\(selectedIndex + 1)/\(images.count)"

        }),

        (AppRouter.discovery, { _ in AnyView(Text("发现页面")) }, { _ in "发现" }),

        (AppRouter.discoveryDetail, { args in

            AnyView(Text(args["title"] as? String ?? "发现详情"))

        }, { args in args["title"] as? String ?? "发现详情" }),

        (AppRouter.collection, { _ in AnyView(Text("我的收藏")) }, { _ in "我的收藏" }),

        (AppRouter.notification, { _ in AnyView(Text("消息通知")) }, { _ in "消息通知" }),

        (AppRouter.editProfile, { _ in AnyView(Text("编辑个人资料")) }, { _ in "编辑个人资料" }),

        

        // 系统相关组件

        (AppRouter.animatePage, { _ in AnyView(AnimatePageView()) }, { _ in "动画页面" }),

        (AppRouter.component, { _ in AnyView(ComponentView()) }, { _ in "组件" }),

        (AppRouter.customeModifier, { _ in AnyView(CustomeModifierView()) }, { _ in "自定义修饰符" }),

        (AppRouter.dynamicContent, { _ in AnyView(DynamicContentView()) }, { _ in "动态内容" }),

        (AppRouter.geometryReader, { _ in AnyView(GeometryReaderView()) }, { _ in "几何阅读器" }),

        (AppRouter.gesture, { _ in AnyView(GestureView()) }, { _ in "手势" }),

        (AppRouter.nav, { _ in AnyView(NavView()) }, { _ in "导航" }),

        

        // 自定义组件

        (AppRouter.wrap, { _ in AnyView(WrapDemo()) }, { _ in "Wrap示例" }),

        (AppRouter.circleLayout, { _ in AnyView(CircleLayoutDemo()) }, { _ in "Circle示例" }),

        (AppRouter.pager, { _ in AnyView(PagerViewDemo()) }, { _ in "分页视图" }),

        (AppRouter.unknow, { _ in AnyView(UnknowView()) }, { _ in "未知页面" }),

        (AppRouter.custom, { _ in AnyView(CustomView()) }, { _ in "自定义视图" }),

        (AppRouter.test, { _ in AnyView(TabTestView()) }, { _ in "测试页面" }),

        (AppRouter.imageGalleryDemo, { _ in AnyView(ImageGalleryDemo()) }, { _ in "图片画廊" }),

        (AppRouter.notificationBannerView, { _ in AnyView(NotificationBannerView()) }, { _ in "导航栏通知" }),

        (AppRouter.pickerViewPage, { _ in AnyView(PickerViewPage()) }, { _ in "选择" }),

        (AppRouter.fileHelperDemo, { _ in AnyView(FileHelperDemo()) }, { _ in "文件选择" }),

  


        (AppRouter.modelCodablePage, { _ in AnyView(ModelCodablePage()) }, { _ in "模型解析" }),

        (AppRouter.locationDemo, { _ in AnyView(LocationDemo()) }, { _ in "地图功能" }),

        (AppRouter.dataTypeDemo, { _ in AnyView(DataTypeDemo()) }, { _ in "数据类型" }),

    ]
}

  

class AppPage<T: View>: Hashable {

    let id = UUID()

    let route: String

    let title: String

    let viewBuilder: ([String: Any]) -> T

    var arguments: [String: Any]
   
    init(route: String, title: String, view: @escaping ([String: Any]) -> T, arguments: [String: Any] = [:]) {
        self.route = route
        self.title = title
        self.viewBuilder = view
        self.arguments = arguments
    }

    func makeView() -> AnyView {
        AnyView(viewBuilder(arguments))
    }

    static func == (lhs: AppPage<T>, rhs: AppPage<T>) -> Bool {
        lhs.id == rhs.id
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
    }
}

// 路由注册表
@MainActor class RouteRegistry {

    @MainActor static let shared = RouteRegistry()

    private var routes: [String: ([String: Any]) -> AppPage<AnyView>] = [:]

    private init() {
        registerPages(pages: AppRouter.pages)
    }

    func register<T: View>(route: String, builder: @escaping ([String: Any]) -> AppPage<T>) {

        routes[route] = { args in
            let page = builder(args)
            return AppPage(
                route: page.route,
                title: page.title,
                view: { args in AnyView(page.viewBuilder(args)) },
                arguments: args
            )
        }
    }

    func page(for route: String, arguments: [String: Any] = [:]) -> AppPage<AnyView>? {
        return routes[route]?(arguments)
    }

    /// 注册
    private func registerPages(pages: [(String, ([String: Any]) -> AnyView, ([String: Any]) -> String)]) {

        // 注册所有页面
        for (route, viewBuilder, titleBuilder) in pages {
            register(route: route) { args in
                AppPage(
                    route: route,
                    title: titleBuilder(args),
                    view: { _ in viewBuilder(args) }
                )
            }
        }
    }
}

// 路由中间件协议
protocol RouterMiddleware {
    func redirect<T: View>(_ page: AppPage<T>) -> AppPage<T>?
}

// 路由管理器
@MainActor class Router: ObservableObject {

    @MainActor static let shared = Router()

    private init() {}

   
    @Published var selectedTab: Int = 0

    @Published var pathTabs = [
        NavigationPath(),
        NavigationPath(),
        NavigationPath(),
        NavigationPath(),
        NavigationPath(),
    ]

    @Published var isPresented: Bool = false
    
    private var middlewares: [RouterMiddleware] = []

    @Published var historyTabs: [[AppPage<AnyView>]] = [[], [], [], [], []]

    /// 当前tab导航
    var path: NavigationPath {
        get {
            return pathTabs[selectedTab]
        }
        set {
            pathTabs[selectedTab] = newValue
        }
    }

    /// 当前tab历史
    var historys: [AppPage<AnyView>] {
        get {
            return historyTabs[selectedTab]
        }
        set {
            historyTabs[selectedTab] = newValue
        }
    }

    var routes: [String] {
        return historys.map({ e in
            return e.route
        });
    }

    var routeNames: [String] {
        return historys.map({ e in
            return String("\(e.route)".split(separator: ".").last ?? "");
        });
    }

    // 添加中间件
    func addMiddleware(_ middleware: RouterMiddleware) {
        middlewares.append(middleware)
    }

    // 通过路由导航
    func toNamed(_ route: String, arguments: [String: Any] = [:]) {
        guard let page = RouteRegistry.shared.page(for: route, arguments: arguments) else {
            print("⚠️ Route not found: \(route)")
            return
        }

        

        // 执行中间件
        var finalPage = page
        for middleware in middlewares {
            if let redirected = middleware.redirect(page) {
                finalPage = redirected
                break
            }
        }

        withAnimation {
            isPresented = false
            historys.append(finalPage)
            path.append(finalPage)
            log(prefix: "push >>> ")  
            withAnimation {
                isPresented = true
            }
        }
    }

    // 返回上一页
    func back(count: Int = 1) {
        withAnimation {
            isPresented = false
            if historys.count >= count {
                historys.removeLast(count)
                path.removeLast(count)
            }
            log(prefix: "pop >>> ")
            withAnimation {
                isPresented = true
            }
        }
    }

    // 返回到根页面
    func backToRoot() {
        back(count: self.path.count)
    }

    // 获取当前页面
    var currentPage: AppPage<AnyView>? {
        return historys.last
    }

    // 获取当前参数
    var currentArgs: [String: Any]? {
        currentPage?.arguments
    }

    func log(prefix: String = ""){
        let tmps = pathTabs.map { p in
            if let i = pathTabs.firstIndex(of: p) {
                return "\(i)_\(p.count)"
            }
            return ""
        }
        DDLog("\(prefix) path: \(tmps.joined(separator: ",")), routes: \(routeNames)")
    }
}

extension View {

    func navigationBarCustom(title: String, titleColor: Color = .primary, hideBack: Bool = false, onBack: (() -> Void)? = nil) -> some View {
        modifier(NavigationBarModifier(title: title, titleColor: titleColor, hideBack: hideBack, onBack: onBack))
    }

    func scaleTransitionCustom(isPresented: Bool) -> some View {
        modifier(ScaleTransition(isPresented: isPresented))
    }
}

// 视图修饰器,用于添加导航标题和返回按钮

struct NavigationBarModifier: ViewModifier {

    let title: String

    let titleColor: Color

    let hideBack: Bool

    let onBack: (() -> Void)?

    @Environment(\.dismiss) private var dismiss

    @ObservedObject private var router = Router.shared

    init(title: String, titleColor: Color = .primary, hideBack: Bool = false, onBack: (() -> Void)? = nil) {
        self.title = title
        self.titleColor = titleColor
        self.hideBack = hideBack
        self.onBack = onBack
    }

    func body(content: Content) -> some View {
        content
            .navigationTitle(title)
            .navigationBarTitleDisplayMode(.inline)
//            .toolbarColorScheme(.dark, for: .navigationBar)
//            .toolbarBackground(.black, for: .navigationBar)
//            .toolbarBackground(.visible, for: .navigationBar)
            .navigationDestination(for: AppPage<AnyView>.self) { page in
                page.makeView()
            }
            .navigationBarBackButtonHidden(true)
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    if !hideBack {
                        Button {
                            onBack?() ?? router.back()
                        } label: {
                            Image(systemName: "chevron.left")
                                .foregroundColor(titleColor)
                        }
                    }
                }
            }
            .foregroundColor(titleColor)
    }
}

// 自定义转场效果

struct ScaleTransition: ViewModifier {

    let isPresented: Bool

    func body(content: Content) -> some View {
        content
            .scaleEffect(isPresented ? 1 : 0.95)
            .opacity(isPresented ? 1 : 0)
            .animation(.spring(response: 0.35, dampingFraction: 1), value: isPresented)
    }
}

四、最后

目前只是初步实现,但是近期技术方向偏向 flutter,先记录一下。

github

相关推荐
brzhang24 分钟前
我操,终于有人把 AI 大佬们 PUA 程序员的套路给讲明白了!
前端·后端·架构
止观止1 小时前
React虚拟DOM的进化之路
前端·react.js·前端框架·reactjs·react
goms1 小时前
前端项目集成lint-staged
前端·vue·lint-staged
谢尔登1 小时前
【React Natve】NetworkError 和 TouchableOpacity 组件
前端·react.js·前端框架
Lin Hsüeh-ch'in1 小时前
如何彻底禁用 Chrome 自动更新
前端·chrome
augenstern4163 小时前
HTML面试题
前端·html
张可3 小时前
一个KMP/CMP项目的组织结构和集成方式
android·前端·kotlin
G等你下课4 小时前
React 路由懒加载入门:提升首屏性能的第一步
前端·react.js·前端框架
蓝婷儿5 小时前
每天一个前端小知识 Day 27 - WebGL / WebGPU 数据可视化引擎设计与实践
前端·信息可视化·webgl
然我5 小时前
面试官:如何判断元素是否出现过?我:三种哈希方法任你选
前端·javascript·算法