第三十章 接下来我们写首页的功能,首先是我们的`托盘绑定箱号`。

创建托盘绑定箱号界面

新建 ViewModel

swift 复制代码
class PalletBindBoxNumberPageViewModel: BaseViewModel {   
}

新建 Page

swift 复制代码
struct PalletBindBoxNumberPage: View {
    @StateObject private var viewModel = PalletBindBoxNumberPageViewModel()
    var body: some View {
        PageContentView(title: "托盘绑定箱号", viewModel: viewModel) {
            EmptyView()
        }
    }
}

新增首页跳转 PalletBindBoxNumberPage

对于导航的跳转,我们需要用到NavigationLink.

swift 复制代码
struct HomePage: View {
    ...
    var body: some View {
        ... {
            ... {
                ActionCardView(
                    title: "生产执行",
                    actions: [
                    	/// ActionItem
                        ...
                ])
                 ...
            }
        } 
        ...
}
swift 复制代码
struct ActionItem: Hashable {
    ...
}

ActionItem不是一个View,因此不能够使用NavigationLink

swift 复制代码
class HomePageViewModel: BaseViewModel {
    ...
    /// 是否允许跳转界面
    @Published var isAllowPushPage:Bool = false
    ...
}
swift 复制代码
struct HomePage: View {
    ...
    var body: some View {
        PageContentView(title: "首页",
                        viewModel: viewModel) {
            VStack {
                ...
                VStack(spacing:0) {
                    NavigationLink(isActive: $viewModel.isAllowPushPage) {
                        
                    } label: {
                        EmptyView()
                    }
                    Spacer()
                }
            }
        } leadingBuilder: {
            ...
        } trailingBuildeder: {
            ...
        }
        ...
    }
}

获取点击首页按钮 ActionItem

HomePageViewModel 新增记录选中 ActionItem的变量。
swift 复制代码
class HomePageViewModel: BaseViewModel {
    ...
    /// 当前点击按钮的 `ActionItem`
    @Published var currentClickActionItem:ActionItem?
    ...
}
ActionCardView
swift 复制代码
struct ActionCardView: View {
    ...
    @Binding var currentClickActionItem:ActionItem?
    var body: some View {
        VStack {
            ...
            HStack(alignment:.top) {
                HStack {
                    ActionView(actionItems: actions(index: .left),
                               currentClickActionItem: $currentClickActionItem)
                    ...
                }
                ...
                HStack {
                    ActionView(actionItems: actions(index: .center),
                               currentClickActionItem: $currentClickActionItem)
                }
                ...
                HStack {
                    ...
                    ActionView(actionItems: actions(index: .right),
                               currentClickActionItem: $currentClickActionItem)
                }
                ...
            }
            ...
        }
        ...
    }
		...
}
ActionView
swift 复制代码
struct ActionView: View {
    ...
    @Binding var currentClickActionItem:ActionItem?
    var body: some View {
        VStack {
            ForEach(actionItems, id: \.self) { item in
                ...
                    .onTapGesture {
                        currentClickActionItem = item
                    }
            }
        }
    }
}
HomePage
swift 复制代码
struct HomePage: View {
    ...
    var body: some View {
        PageContentView(title: "首页",
                        viewModel: viewModel) {
            VStack {
                ActionCardView(
                    title: "生产执行",
                    actions: [
                        ....
                    ], currentClickActionItem: $viewModel.currentClickActionItem)
                    ...
                }
            }
        } leadingBuilder: {
            ...
        } trailingBuildeder: {
            ...
        }
        .onAppear {
            ...
        }
    }
}

监听 currentClickActionItem 值的改变,执行跳转。

swift 复制代码
struct HomePage: View {
    ...
    var body: some View {
        PageContentView(title: "首页",
                        viewModel: viewModel) {
            ...
        } leadingBuilder: {
            ...
        } trailingBuildeder: {
            ...
        }
        ...
        .onChange(of: viewModel.currentClickActionItem) { newValue in
            viewModel.isAllowPushPage = true
        }
    }
}

根据 ActionItem 返回对应的 Page

swift 复制代码
extension HomePageViewModel {
    var actionPage: some View {
        return currentClickActionItem.map { item in
            Group {
                if item.title == "托盘绑定箱号" {
                    PalletBindBoxNumberPage()
                } else {
                    EmptyView()
                }
            }
        }
    }
}

HomePage

swift 复制代码
struct HomePage: View {
    ...
    var body: some View {
        PageContentView(title: "首页",
                        viewModel: viewModel) {
            VStack {
                ...
                VStack(spacing:0) {
                    NavigationLink(isActive: $viewModel.isAllowPushPage,
                                   destination: {viewModel.actionPage}) {
                        EmptyView()
                    }
                    ...
                }
            }
        } leadingBuilder: {
            ...
        } trailingBuildeder: {
            ...
        }
        ...
    }
}

修复返回按钮样式不对

隐藏返回按钮文本

swift 复制代码
let backButtonAppearance = UIBarButtonItemAppearance()
backButtonAppearance.normal.titleTextAttributes = [
    .font : UIFont.systemFont(ofSize: 0),
]
appearance.backButtonAppearance = backButtonAppearance

修改 SwiftUI 返回按钮的颜色

markdown 复制代码
NavigationView {
	...
}
.accentColor(.black)

需要注意的是官方说accentColor已经要废弃了,Use the asset catalog's accent color or View.tint(_:) instead."

但是替换为 tint不起作用。

没有隐藏底部的 Tab

目前在 SwiftUI中暂时没有任何方便的方法可以在 NavigationView 进行 Push 跳转隐藏底部的 Tabbar。我们只能在需要隐藏的界面的 onAppearonDisappear去隐藏。

swift 复制代码
/// ❌ 这样设置是不起作用的
UITabBar.appearance().isHidden = true

我们在运行时候,看一下布局。

我们按照结构找出 UITabbar

swift 复制代码
if let appBar = App.keyWindow?.rootViewController
    .flatMap({$0.view})
    .flatMap({$0.subviews.first})
    .flatMap({$0.subviews.first})
    .map({$0.subviews})
    .map({$0.compactMap({$0 as? UITabBar})})
    .flatMap({$0.first}) {
    print(appBar)
}

App 获取当前 Tabbar 的方法

swift 复制代码
struct App {
    ...
    
    static var tabBar:UITabBar? {
        return keyWindow?.rootViewController
            .flatMap({$0.view})
            .flatMap({$0.subviews.first})
            .flatMap({$0.subviews.first})
            .map({$0.subviews})
            .map({$0.compactMap({$0 as? UITabBar})})
            .flatMap({$0.first})
    }
}

隐藏和显示当前 UITabbar

swift 复制代码
struct PalletBindBoxNumberPage: View {
    ...
    var body: some View {
        PageContentView(title: "托盘绑定箱号", viewModel: viewModel) {
            ...
        }
        .onAppear {
            App.tabBar?.isHidden = false
        }
        .onDisappear {
            App.tabBar?.isHidden = true
        }
    }
}

隐藏UITabar之后多出了很多空白的区域,我们设置忽略安全距离。

swift 复制代码
/// 页面的基础试图
struct PageContentView<Content:View,
                        Leading:View,
                        Trailing:View,
                        ViewModel:BaseViewModel>: View {
    ...
    var body: some View {
        navigationBar {
            ZStack {
                content
                    .background {
                        Color(uiColor: appColor.c_efefef)
                            .ignoresSafeArea()
                    }
            }
            ...
        }
    }
    ...
}

封装 Detail 页面

为了让后面的界面一样拥有 隐藏UITabBar我们需要进行封装成DetailView,方便后续的使用。

新建一个 DetailPageViewModify

swift 复制代码
struct DetailPageViewModify: ViewModifier {
    func body(content: Content) -> some View {
        content
            .onAppear {
                App.tabBar?.isHidden = true
            }
            .onDisappear {
                App.tabBar?.isHidden = false
            }
    }
}

extension View {
    func makeToDetailPage() -> some View {
        self.modifier(DetailPageViewModify())
    }
}

将 PalletBindBoxNumberPage 页面使用 DetailPageViewModify

swift 复制代码
struct PalletBindBoxNumberPage: View {
    @StateObject private var viewModel = PalletBindBoxNumberPageViewModel()
    var body: some View {
        PageContentView(title: "托盘绑定箱号", viewModel: viewModel) {
            EmptyView()
        }
        .makeToDetailPage()
    }
}

修复第二次相同页面无法 Push 问题

从上面的掩饰发现,第一次是可以正常的进入,点击返回,第二次无法Push进入。只有点击其他页面返回之后,才能正常的返回。

打印点击 Push 对应的 ActionItem

swift 复制代码
newValue = Optional(Win_.ActionItem(icon: "托盘绑定箱号", iconColor: UIExtendedSRGBColorSpace 0.945098 0.564706 0.215686 1, title: "托盘绑定箱号"))
newValue = Optional(Win_.ActionItem(icon: "灭菌整板(有箱号)", iconColor: UIExtendedSRGBColorSpace 0.945098 0.564706 0.215686 1, title: "灭菌整板(有箱号)"))
newValue = Optional(Win_.ActionItem(icon: "托盘绑定箱号", iconColor: UIExtendedSRGBColorSpace 0.945098 0.564706 0.215686 1, title: "托盘绑定箱号"))

发现在整个过程中,点击第二次是没有走 onChange,是因为检测到值相同,是因为ActionItem实现了Hashable协议。

在 HomePage 的 onAppear 方法重置 currentClickActionItem

swift 复制代码
struct HomePage: View {
    ...
    var body: some View {
        ...
        .onAppear {
            ...
            viewModel.currentClickActionItem = nil
        }
        ...
    }
}

经过重置,第二次Push无法跳转问题解决了。

封装扫描输入组件

接下来我们封装上面的组件,大致的界面构造如下。

新建一个 ScanTextView

swift 复制代码
struct ScanTextView: View {
    @StateObject private var appColor = AppColor.share
    /// 前面的标题
    private let title:String
    /// 输入框的提示文本
    private let prompt:String
    /// 输入框输入的内容
    @Binding private var text:String
    init(title:String, prompt:String, text:Binding<String>) {
        self.title = title
        self.prompt = prompt
        self._text = text
    }
    var body: some View {
        HStack {
            HStack {
                Text("*")
                    .foregroundColor(Color(uiColor: appColor.c_e68181))
                Text(title)
              Spacer()
            }
            TextField(prompt, text: $text)
                .frame(height:33)
            Image("scan_icon", bundle: .main)
        }
        .font(.system(size: 14))
        .padding()
    }
}

添加栈版号和箱号

swift 复制代码
struct PalletBindBoxNumberPage: View {
    ...
    var body: some View {
        PageContentView(title: "托盘绑定箱号", viewModel: viewModel) {
            VStack {
                VStack {
                    ScanTextView(title: "栈版号",
                                 prompt: "请输入栈版号",
                                 text: $viewModel.palletNumber)
                    ScanTextView(title: "箱号",
                                 prompt: "请输入箱号",
                                 text: $viewModel.boxNumber)
                }
                .background(.white)
                Spacer()
            }
        }
        ...
    }
}
swift 复制代码
class PalletBindBoxNumberPageViewModel: BaseViewModel {
    /// 输入的栈版号
    @Published var palletNumber:String = ""
    /// 箱号
    @Published var boxNumber:String = ""
}

固定 ScanTextView 的 Title 的宽度

提示语是没有对齐的,因为是自动布局,很难会让自动的对齐,我们需要设置左侧标题固定长度。

swift 复制代码
struct ScanTextView: View {
    ...
    /// 默认为 100
    private let titleWidth:CGFloat
    init(title:String, prompt:String, text:Binding<String>, titleWidth:CGFloat = 100) {
        ...
        self.titleWidth = titleWidth
    }
    var body: some View {
        HStack {
            HStack {
                ...
            }
            .frame(width: titleWidth)
            ...
        }
        ...
    }
}

栈版号和箱号中间添加分割线

swift 复制代码
struct PalletBindBoxNumberPage: View {
    @StateObject private var viewModel = PalletBindBoxNumberPageViewModel()
    var body: some View {
        PageContentView(title: "托盘绑定箱号", viewModel: viewModel) {
            VStack(spacing:0) {
                VStack(spacing:0) {
                    ...
                    Divider()
                        .padding(.leading)
                   ...
                }
                ...
            }
        }
        ...
    }
}

箱号详情组件

分析布局如下。

swift 复制代码
struct BoxDetailView: View {
    var body: some View {
        HStack {
            VStack {
                HStack {
                    Text("物料编号:")
                    Text("A")
                }
                HStack {
                    Text("物料批号:")
                    Text("120211217A")
                }
            }
            VStack {
                HStack {
                    Text("工单号:")
                    Text("WO-201425")
                }
                HStack {
                    Text("箱号:")
                    Text("BOX-01")
                }
            }
        }
        .padding(15)
        .frame(maxWidth: .infinity)
        .background(.white)
        .cornerRadius(10)
    }
}

制作标题信息组件

我们需要标题和信息上对齐,类似下面的排版方案。

swift 复制代码
struct TitleValueView: View {
    @StateObject private var appColor = AppColor.share
    private let title:String
    private let value:String
    init(title:String, value:String) {
        self.title = title
        self.value = value
    }
    var body: some View {
        HStack(alignment:.firstTextBaseline) {
            Text(title)
                .foregroundColor(Color(uiColor: appColor.c_999999))
            Text(value)
                .foregroundColor(Color(uiColor: appColor.c_333333))
        }
        .font(.system(size: 14))
        .frame(maxWidth: .infinity, alignment: .leading)
    }
}

将箱号详情标题和描述替换为 TitleValueView 组件

swift 复制代码
struct BoxDetailView: View {
    var body: some View {
        HStack {
            VStack {
                TitleValueView(title: "物料编号:",
                               value: "A")
                TitleValueView(title: "物料批号:",
                               value: "120211217A")
            }
            VStack {
                TitleValueView(title: "工单号:",
                               value: "WO-201425")
                TitleValueView(title: "箱号:",
                               value: "BOX-01")
            }
        }
        ...
    }
}

调整上下组件的间距

swift 复制代码
struct BoxDetailView: View {
    var body: some View {
        HStack {
            VStack {
                ...
                Spacer()
                    .frame(height: 7.5)
                ...
            }
            VStack {
                ...
                Spacer()
                    .frame(height: 7.5)
                ...
            }
        }
        ...
    }
}

可手动控制 Title 的宽度

我们给TitleValueView新增一个可以手动控制Title宽度的参数,如果不为0则手动控制高度。

swift 复制代码
struct TitleValueView: View {
    ...
    private let titleWidth:CGFloat
    init(title:String, value:String, titleWidth:CGFloat = 0) {
        ...
        self.titleWidth = titleWidth
    }
    var body: some View {
        HStack(alignment:.firstTextBaseline) {
            if titleWidth == 0 {
                titleText
            } else {
                titleText
                    .frame(width: titleWidth, alignment: .leading)
            }
            ...
        }
        ...
    }
    
    private var titleText: some View {
        Text(title)
            .foregroundColor(Color(uiColor: appColor.c_999999))
    }
}

我们将工单号和箱号宽度保持一致

swift 复制代码
struct BoxDetailView: View {
    var body: some View {
        HStack {
            VStack {
                ...
            }
            VStack {
                TitleValueView(title: "工单号:",
                               value: "WO-201425",
                               titleWidth: 50)
                ...
                TitleValueView(title: "箱号:",
                               value: "BOX-01",
                               titleWidth: 50)
            }
        }
        ...
    }
}

固定 ScanTextView的高度

经过自动布局之后的ScanTextView的高度达到了65的高度,超出了设计图50的高度,主要是输入框固定了高度,我们将去掉Padding,给ScanTextView设置固定高度为50

swift 复制代码
struct ScanTextView: View {
    ...
    var body: some View {
        HStack {
            ...
        }
        ...
        .frame(height:50)
    }
}

只增加左右间距

高度50设置完毕,但是左右靠边,我们只设置边距左右为10

swift 复制代码
struct ScanTextView: View {
    ...
    var body: some View {
        HStack {
            ...
        }
        ...
        .padding(.leading, 10)
        .padding(.trailing, 10)
      	/// 或者
      	/// .padding(EdgeInsets(top: 0, leading: 10, bottom: 0, trailing: 10))
    }
}

获取箱号列表

新增 @Published 参数箱号列表 用于更新列表

swift 复制代码
class PalletBindBoxNumberPageViewModel: BaseViewModel {
    ...
    /// 箱子列表
    @Published var boxDetailModels:[BoxDetailModel] = []
}

新增根据栈版号获取箱号列表方法

swift 复制代码
class PalletBindBoxNumberPageViewModel: BaseViewModel {
    ...
    /// 请求获取箱子列表
    func requestBoxDetailList() async {
        let api = PalletQueryApi(palletCode: palletNumber)
        let model:BaseModel<[BoxDetailModel]> = await request(api: api)
        guard model._isSuccess else { return }
        boxDetailModels = model.data ?? []
    }
}

当输入栈版号结束之后请求箱号列表

怎么才能监听到输入完毕呢?我们可以使用onSubmit这个扩展获取。

swift 复制代码
struct PalletBindBoxNumberPage: View {
    ...
    var body: some View {
        PageContentView(title: "托盘绑定箱号", viewModel: viewModel) {
            VStack(spacing:0) {
                VStack(spacing:0) {
                    ScanTextView(title: "栈版号",
                                 prompt: "请输入栈版号",
                                 text: $viewModel.palletNumber)
                        .onSubmit {
                            Task {
                                await viewModel.requestBoxDetailList()
                            }
                        }
                    ...
                }
                ...
            }
        }
        ...
    }
}

添加或者删除箱号

此时我们的栈板上是没有数据的,需要我们输入箱号进行新增和删除操作。

上图的逻辑都封装在接口里面,所以我们只需要关心输入箱号之后,调用接口即可。

添加新增或者删除箱号逻辑方法

swift 复制代码
class PalletBindBoxNumberPageViewModel: BaseViewModel {
    ...
    /// 添加或者移除箱号
    func addOrRemoveBox() async {
        let api = BoxAddApi(palletCode: palletNumber, boxCode: boxNumber)
        let model:BaseModel<String> = await request(api: api)
        guard model._isSuccess else {return}
        /// 重新获取列表 刷新界面
        await requestBoxDetailList()
    }
}

给箱号输入框添加onSubmit方法

swift 复制代码
struct PalletBindBoxNumberPage: View {
    ...
    var body: some View {
        PageContentView(title: "托盘绑定箱号", viewModel: viewModel) {
            VStack(spacing:0) {
                VStack(spacing:0) {
                   ...
                    ScanTextView(title: "箱号",
                                 prompt: "请输入箱号",
                                 text: $viewModel.boxNumber)
                        .onSubmit {
                            Task {
                                await viewModel.addOrRemoveBox()
                            }
                        }
                }
                ...
            }
        }
        ...
    }
}

给请求添加HUD

此时添加箱号成功了

json 复制代码
{"code":200,"data":"箱号ç>>‘å®šæ ˆæ¿æˆåŠŸ!!!","message":"success","objectType":null,"success":true}

在日志也看不出来乱码显示,我们希望提示给用户。

给获取箱子列表添加HUD

swift 复制代码
class PalletBindBoxNumberPageViewModel: BaseViewModel {
    ...
    /// 请求获取箱子列表
    func requestBoxDetailList() async {
        ...
        let model:BaseModel<[BoxDetailModel]> = await request(api: api, showHUD: true)
        ...
    }
    ...
}

此时我们已经获取到列表了,但是HUD没有消失,主要是逻辑中没有调用隐藏HUD。

给BaseViewModel新增Hidden HUD方法

swift 复制代码
@MainActor
class BaseViewModel: ObservableObject {
    ...
    func hiddenHUD() {
        self.isLoadingHUD = false
    }
    ...
}

给查询箱号和新增和删除箱号添加HUD和移除HUD

swift 复制代码
class PalletBindBoxNumberPageViewModel: BaseViewModel {
    ...
    
    /// 请求获取箱子列表
    func requestBoxDetailList() async {
        ...
        hiddenHUD()
        ...
    }
    
    /// 添加或者移除箱号
    func addOrRemoveBox() async {
       ...
        let model:BaseModel<String> = await request(api: api, showHUD: true)
        ...
        hiddenHUD()
        ...
    }
}

添加或者删除成功提示

上面的代码我们还是无法成功显示提示语,到底是添加成功还是删除成功。当我们请求完毕,展示获取的Data字符串。

但是展示和隐藏十分的快,在显示没有结束之前,被后面获取箱子列表接口在请求完毕之后隐藏了。

swift 复制代码
class PalletBindBoxNumberPageViewModel: BaseViewModel {
    ...
    
    /// 添加或者移除箱号
    func addOrRemoveBox() async {
        ...
        if let message = model.data {
            showHUDMessage(message: message)
        }
    }
}

修复HUD开始显示之前内容的问题

HUD展示逻辑

HUD Message展示逻辑

我们看到在展示文本延时两秒之后,文本没有清空,导致下次请求进行Loading时候因为文本不为空,展示不是一个Loading HUD而是上一个提示的文本。

清空上一个展示的文本

修复这个问题,大概有两种方案

方案1 在延时两秒隐藏时候 清空文本

swift 复制代码
@MainActor
class BaseViewModel: ObservableObject {
    ...
    
    /// 展示 HUD 文本
    /// - Parameter message: 提示的信息
    func showHUDMessage(message:String) {
        ...
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            ...
            self.hudMessage = ""
        }
    }
    
    ...
}

方案2 在展示HUD的时候 清空之前的文本

swift 复制代码
@MainActor
class BaseViewModel: ObservableObject {
    ...
    func request<T:Codable, API:APIConfig>(api:API, showHUD:Bool = false) async -> BaseModel<T> {
        if (showHUD) {
            hudMessage = ""
            ...
        }
        ...
    }
}

展示HUD Message的文本内容只是一个临时的展示内容,应该在展示完毕重置,所以第一种方案比较好。

相关推荐
君赏4 小时前
第三十二章 接下来我们开始做`灭菌整板`页面
swiftui
君赏4 小时前
第三十一章 完善箱号列表
swiftui
君赏4 小时前
第二十五章 完善登录逻辑
swiftui
君赏4 小时前
第二十六章 Focused
swiftui
君赏4 小时前
第 二十章 @Published sink
swiftui
君赏4 小时前
第二十一章 @ViewBuilder默认实现|Toggle|我的页面封装
swiftui
君赏4 小时前
第二十九章 修复首页 PopMenuView 显示问题
swiftui
君赏4 小时前
第二十八章 重置 ObservableObject 模型数据
swiftui
君赏4 小时前
第二十七章 UINavigationBarAppearance|Divider
swiftui