第二十一章 @ViewBuilder默认实现|Toggle|我的页面封装

首页的界面基本做完了,功能也挺简单,跳转到对应界面即可。我们就先做一下我的页面的内容,内容也不是很多。

我的页面是一个配置和显示的功能也不是很复杂,但是界面也需要标题栏和灰色的背景试图。但是我们就需要将首页的代码复制一份过来吗?在UIKit的时代,因为是继承关系,我们可以在父类进行设置,但是现在我们在SwiftUI里面。Struct是不能继承的,我们只能封装,使用的时候使用封装的组件来达到效果。

在封装的过程中,遇到了一些困难,差一点就放弃了封装,幸亏找到了解决思路。

遇到的困难就是对于 @ViewBuilder 怎么在初始化提供默认的实现,因为有一些有一些封装不是必须实现的,下面的链接提供了解决的方法。

stackoverflow.com/questions/6...

swift 复制代码
/// 页面的基础试图
struct PageContentView<Content:View, Leading:View, Trailing:View>: View {
    
    private let title:String
    private let content:Content
    private let leading:Leading
    private let trailing:Trailing
    
    @StateObject private var appColor:AppColor = AppColor.share
    
    /// 初始化页面试图
    /// - Parameters:
    ///   - title: 导航标题
    ///   - contentBuilder: 内容
    ///   - leadingBuilder: 导航左侧按钮
    ///   - trailingBuildeder: 导航右侧按钮
    init(title:String,
         @ViewBuilder contentBuilder:() -> Content,
         @ViewBuilder leadingBuilder:() -> Leading,
         @ViewBuilder trailingBuildeder:() -> Trailing) {
        self.title = title
        self.content = contentBuilder()
        self.leading = leadingBuilder()
        self.trailing = trailingBuildeder()
    }
    
    var body: some View {
        navigationBar {
            ZStack {
                Color(uiColor: appColor.c_efefef)
                content
            }
        }
    }
    
    private func navigationBar<Content:View>(@ViewBuilder content:() -> Content) -> some View {
        content()
            .navigationTitle(Text(title))
            .navigationBarTitleDisplayMode(.inline)
            .toolbar {
                ToolbarItem(placement:.navigationBarLeading) {leading}
                ToolbarItem(placement:.navigationBarTrailing) {trailing}
            }
    }
}

extension PageContentView where Leading == EmptyView, Trailing == EmptyView {
    init(title:String, contentBuilder:() -> Content) {
        self.init(title: title,
                  contentBuilder: contentBuilder,
                  leadingBuilder: {EmptyView()},
                  trailingBuildeder: {EmptyView()})
    }
}

我们将刚才封装 PageContentView 用到我们的首页。

swift 复制代码
struct HomePage: View {
    ...
    var body: some View {
        NavigationView {
            PageContentView(title: "首页") {
                ...
            } leadingBuilder: {
                ...
            } trailingBuildeder: {
                EmptyView()
            }

        }
        ...
    }
}

我们封装完毕 PageContentView 完毕之后,我们想让登录页面也统一走这个封装试图,我们尝试的修改一下登录界面。

swift 复制代码
struct LoginPage: View {
		...
    var body: some View {
        PageContentView(title: "登陆") {
            VStack {
                ...
            }
            .background(.white)
        }
        ...
    }
}

我们登陆页面使用 PageContentView 之后,背景颜色会变成灰色,我们重新设置一下内容区域颜色即可。

之前在封装登陆页面的时候,对于外部 HUDView 一致无法封装,我们现在能否封装在 PageContentView 里面呢?

swift 复制代码
/// 页面的基础试图
struct PageContentView<Content:View,
                        Leading:View,
                        Trailing:View,
                        ViewModel:BaseViewModel>: View {
    
    ...
    @ObservedObject private var viewModel:ViewModel
    ...
    
    /// 初始化页面试图
    /// - Parameters:
    ///   - title: 导航标题
    ///   - contentBuilder: 内容
    ///   - leadingBuilder: 导航左侧按钮
    ///   - trailingBuildeder: 导航右侧按钮
    init(title:String,
         viewModel:ViewModel,
         @ViewBuilder contentBuilder:() -> Content,
         @ViewBuilder leadingBuilder:() -> Leading,
         @ViewBuilder trailingBuildeder:() -> Trailing) {
        ...
        self.viewModel = viewModel
        ...
    }
    
    var body: some View {
        navigationBar {
            ZStack {
                Color(uiColor: appColor.c_efefef)
                content
            }
            .hud(viewModel: $viewModel)
        }
    }
    
    ...
}

extension PageContentView where Leading == EmptyView, Trailing == EmptyView {
    init(title:String,
         viewModel:ViewModel,
         contentBuilder:() -> Content) {
        self.init(title: title,
                  viewModel: viewModel,
                  contentBuilder: contentBuilder,
                  leadingBuilder: {EmptyView()},
                  trailingBuildeder: {EmptyView()})
    }
}

我们测试将HUD封装在 PageContentView内部,通过登陆页面调试一切正常。现在我们可以开始做我的页面了。

swift 复制代码
struct MyPage: View {
    @StateObject private var viewModel = MyPageViewModel()
    var body: some View {
        PageContentView(title: "我的",
                        viewModel: viewModel) {
            Text("Hello, World!")
        }
    }
}

struct MyPage_Previews: PreviewProvider {
    static var previews: some View {
        NavigationView {
            MyPage()
        }
    }
}

我的页面基本全是这种左右对齐的控件,我们先制作这个控件。

swift 复制代码
struct MyCellContentView: View {
    @StateObject private var appColor = AppColor.share
    var body: some View {
        HStack {
            Text("车间")
                .font(.system(size: 14))
                .foregroundColor(Color(uiColor: appColor.c_333333))
            Spacer()
            HStack {
                Text("深圳车间")
                    .font(.system(size: 14))
                    .foregroundColor(Color(uiColor: appColor.c_333333))
                Image(systemName: "chevron.right")
                    .foregroundColor(Color(uiColor: appColor.c_cccccc))
            }
        }
        .frame(maxWidth: .infinity)
        .padding(EdgeInsets(top: 15, leading: 10, bottom: 15, trailing: 10))
      	.background(.white)
    }
}

我们进行提炼封装,方便后面使用。

swift 复制代码
struct MyCellContentView<Right:View>: View {
    ...
    private let right:Right
    ...
    
    init(title:String,
         @ViewBuilder rightBuilder:() -> Right) {
        self.title = title
        self.right = rightBuilder()
    }
    
    var body: some View {
        HStack {
            Text(title)
                ...
            ...
            right
        }
        ...
    }
}

我们使用上面的组件来给我的页面绘制下面的界面

swift 复制代码
private func userNameCell() -> some View {
    MyCellContentView(title: "姓名") {
        Text("我的名字")
            .font(.system(size: 14))
            .foregroundColor(Color(uiColor: appColor.c_cccccc))
    }
}

我们这里需要展示用户的昵称,但是我们在用户登录的时候没有保存用户的昵称,我们在登录页面写一下保存的逻辑。

swift 复制代码
class AppConfig: ObservableObject {
    ...
    @AppStorage("userInfo")
    private var userInfo:String?
    /// 用户信息
    var userInfoModel:UserInfoModel? {
        get {
            guard let userInfo = userInfo, let jsonData = userInfo.data(using: .utf8) else {
                return nil
            }
            return try? CleanJSONDecoder().decode(UserInfoModel.self, from: jsonData)
        }
        set {
            guard let value = newValue, let jsonData = try? JSONEncoder().encode(value) else {
                userInfo = nil
                return
            }
            userInfo = String(data: jsonData, encoding: .utf8)
        }
    }
    
}
swift 复制代码
class LoginPageViewModel: BaseViewModel {    
    ...
    
    func login() async {
        ...
        AppConfig.share.userInfoModel = model.data?.user
    }
}

我们已经可以拿到保存的用户名了,我们更新一下刚才视图的代码。

swift 复制代码
struct MyPage: View {
    ...
    private func userNameCell() -> some View {
        MyCellContentView(title: "姓名") {
            Text(AppConfig.share.userInfoModel?.userName ?? "")
                ...
        }
    }
}

接下来我们制作下面的视图

swift 复制代码
import SwiftUI

struct MyPage: View {
    ...
    var body: some View {
        PageContentView(title: "我的",
                        viewModel: viewModel) {
            VStack {
                ...
                workshopCell()
                ...
            }
        }
    }
    
    ...
    
    private func workshopCell() -> some View {
        MyCellContentView(title: "车间") {
            HStack {
                Text("深圳车间")
                    .font(.system(size: 14))
                    .foregroundColor(Color(uiColor: appColor.c_333333))
                Image(systemName: "chevron.right")
                    .foregroundColor(Color(uiColor: appColor.c_cccccc))
            }
        }
    }
}

我们发现 MyCellContentView 组件的下面没有显示横线,这个和我们界面不太一样,我们就修改 MyCellContentView 新增一条线。

swift 复制代码
struct MyCellContentView<Right:View>: View {
    ...
    
    var body: some View {
        VStack(spacing: 0) {
            HStack {
                ...
            }
            .frame(maxWidth: .infinity)
            .padding(EdgeInsets(top: 15, leading: 10, bottom: 15, trailing: 10))
            Rectangle()
                .foregroundColor(Color(uiColor: appColor.c_cccccc))
                .frame(height: 0.5)
                .padding(.leading, 15)
        }
        .background(.white)
    }
}

接下来我们做产线界面

我们发现和刚才做的车间的界面一模一样,我们可以先将车间的进行提炼。

css 复制代码
struct MyDetailCellContentView: View {
    @StateObject private var appColor = AppColor.share
    private let title:String
    private let detail:String
    
    init(title:String,
         detail:String) {
        self.title = title
        self.detail = detail
    }
    
    var body: some View {
        MyCellContentView(title: title) {
            HStack {
                Text(detail)
                    .font(.system(size: 14))
                    .foregroundColor(Color(uiColor: appColor.c_333333))
                Image(systemName: "chevron.right")
                    .foregroundColor(Color(uiColor: appColor.c_cccccc))
            }
        }
    }
}
swift 复制代码
struct MyPage: View {
    ...
    private func workshopCell() -> some View {
        MyDetailCellContentView(title: "车间", detail: "深圳车间")
    }
}
swift 复制代码
struct MyPage: View {
    ...
    var body: some View {
        PageContentView(title: "我的",
                        viewModel: viewModel) {
            VStack(spacing: 0) {
                ...
                productLineCell()
                ...
            }
        }
    }
    
    ...
    private func productLineCell() -> some View {
        MyDetailCellContentView(title: "产线", detail: "生产线_yk")
    }
}
swift 复制代码
struct MyPage: View {
    ...
    var body: some View {
        PageContentView(title: "我的",
                        viewModel: viewModel) {
            VStack(spacing: 0) {
                ...
                Spacer()
                    .frame(height:10)
                storeHourseCell()
                ...
            }
        }
    }
    
    ...
    private func storeHourseCell() -> some View {
        MyDetailCellContentView(title: "仓库", detail: "123")
    }
}

Toggle 开关

我们自动登录模块稍微有一些不同,我们右侧是一个开关按钮,我们需要用到Toggle

swift 复制代码
class AppConfig: ObservableObject {
    ...
    /// 是否自动登录
    @AppStorage("isAutoLogin")
    var isAutoLogin = false
}
swift 复制代码
struct MyPage: View {
    ...
    @StateObject private var appConfig = AppConfig.share
    var body: some View {
        PageContentView(title: "我的",
                        viewModel: viewModel) {
            VStack(spacing: 0) {
                ...
                autoLoginCell()
                ...
            }
        }
    }
    
    ...
    private func autoLoginCell() -> some View {
        MyCellContentView(title: "自动登录") {
            Toggle("", isOn: $appConfig.isAutoLogin)
        }
    }
}

显示当前版本,这个可以将显示名称的提炼共用一套。

swift 复制代码
struct MyDetailStyle1CellContentView: View {
    @StateObject private var appColor = AppColor.share
    private let title:String
    private let detail:String
    init(title:String,
         detail:String) {
        self.title = title
        self.detail = detail
    }
    var body: some View {
        MyCellContentView(title: title) {
            Text(detail)
                .font(.system(size: 14))
                .foregroundColor(Color(uiColor: appColor.c_cccccc))
        }
    }
}
swift 复制代码
struct MyPage: View {
    ...
    var body: some View {
        PageContentView(title: "我的",
                        viewModel: viewModel) {
            VStack(spacing: 0) {
                ...
                appVersionCell()
                ...
            }
        }
    }
    
    private func userNameCell() -> some View {
        MyDetailStyle1CellContentView(title: "姓名",
                                      detail: AppConfig.share.userInfoModel?.userName ?? "")
    }
    
    ...
    private func appVersionCell() -> some View {
        MyDetailStyle1CellContentView(title: "版本", detail: "1.2.0(1638264135)")
    }
}
swift 复制代码
struct MyPage: View {
    ...
    var body: some View {
        PageContentView(title: "我的",
                        viewModel: viewModel) {
            VStack(spacing: 0) {
                ...
                logoutButton()
                ...
            }
        }
    }
    
    ...
    private func logoutButton() -> some View {
        Button {
            
        } label: {
            Text("退出登录")
                .frame(maxWidth:.infinity)
                .frame(height:45)
                .background(Color(uiColor: appColor.c_209090))
                .foregroundColor(.white)
                .font(.system(size: 16))
                .cornerRadius(5)
                .padding(30)
        }

    }
}

至此,我的界面算是全部做出来了。但是界面的数据和交互算是我的页面复杂的部分,虽然显示文本很简单,但是数据的获取十分的麻烦。

我们将我的页面添加到 TabPage里面。

swift 复制代码
struct TabPage: View {
    ...
    var body: some View {
        TabView(selection:$currentTabIndex) {
            ...
            MyPage()
                .tabItem {
                    VStack {
                        if currentTabIndex == 1 {
                            Image("我的1")
                        } else {
                            Image("我的2")
                        }
                        Text("我的")
                    }
                }
                .tag(1)
        }
        ...
    }
}
相关推荐
君赏3 小时前
第 二十章 @Published sink
swiftui
君赏3 小时前
第二十九章 修复首页 PopMenuView 显示问题
swiftui
君赏3 小时前
第二十八章 重置 ObservableObject 模型数据
swiftui
君赏3 小时前
第二十七章 UINavigationBarAppearance|Divider
swiftui
君赏3 小时前
第二十二章 onAppear|DataPickerView
swiftui
君赏3 小时前
第二十三章 UIHostingController|withAnimation|SwiftUI 默认动画时间
swiftui
君赏3 小时前
第二十四章 init 方法初始化 State
swiftui
君赏3 小时前
第十九章 TabView|accentColor|AnyView|NavigationView|navigationTitle|navigationBarTit
swiftui
君赏3 小时前
第十七章 @MainActor
swiftui