创建托盘绑定箱号界面
新建 ViewModel
class PalletBindBoxNumberPageViewModel: BaseViewModel {
}
新建 Page
struct PalletBindBoxNumberPage: View {
@StateObject private var viewModel = PalletBindBoxNumberPageViewModel()
var body: some View {
PageContentView(title: "托盘绑定箱号", viewModel: viewModel) {
EmptyView()
}
}
}
新增首页跳转 PalletBindBoxNumberPage
NavigationLink
对于导航的跳转,我们需要用到NavigationLink.
struct HomePage: View {
...
var body: some View {
... {
... {
ActionCardView(
title: "生产执行",
actions: [
/// ActionItem
...
])
...
}
}
...
}
struct ActionItem: Hashable {
...
}
ActionItem不是一个View,因此不能够使用NavigationLink。
在 Function方法体内部执行 NavigationLink跳转。
HomePageViewModel 新增一个控制 NavigationLink 激活的变量
class HomePageViewModel: BaseViewModel {
...
/// 是否允许跳转界面
@Published var isAllowPushPage:Bool = false
...
}
在 HomePage 新增一个不可见的 NavigationLink
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的变量。
class HomePageViewModel: BaseViewModel {
...
/// 当前点击按钮的 `ActionItem`
@Published var currentClickActionItem:ActionItem?
...
}
ActionCardView
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
struct ActionView: View {
...
@Binding var currentClickActionItem:ActionItem?
var body: some View {
VStack {
ForEach(actionItems, id: \.self) { item in
...
.onTapGesture {
currentClickActionItem = item
}
}
}
}
}
HomePage
struct HomePage: View {
...
var body: some View {
PageContentView(title: "首页",
viewModel: viewModel) {
VStack {
ActionCardView(
title: "生产执行",
actions: [
....
], currentClickActionItem: $viewModel.currentClickActionItem)
...
}
}
} leadingBuilder: {
...
} trailingBuildeder: {
...
}
.onAppear {
...
}
}
}
监听 currentClickActionItem 值的改变,执行跳转。
struct HomePage: View {
...
var body: some View {
PageContentView(title: "首页",
viewModel: viewModel) {
...
} leadingBuilder: {
...
} trailingBuildeder: {
...
}
...
.onChange(of: viewModel.currentClickActionItem) { newValue in
viewModel.isAllowPushPage = true
}
}
}
根据 ActionItem 返回对应的 Page
extension HomePageViewModel {
var actionPage: some View {
return currentClickActionItem.map { item in
Group {
if item.title == "托盘绑定箱号" {
PalletBindBoxNumberPage()
} else {
EmptyView()
}
}
}
}
}
HomePage
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: {
...
}
...
}
}
修复返回按钮样式不对
隐藏返回按钮文本
let backButtonAppearance = UIBarButtonItemAppearance()
backButtonAppearance.normal.titleTextAttributes = [
.font : UIFont.systemFont(ofSize: 0),
]
appearance.backButtonAppearance = backButtonAppearance
修改 SwiftUI 返回按钮的颜色
NavigationView {
...
}
.accentColor(.black)
需要注意的是官方说
accentColor已经要废弃了,Use the asset catalog's accent color or View.tint(_:) instead."但是替换为
tint不起作用。
没有隐藏底部的 Tab
目前在 SwiftUI中暂时没有任何方便的方法可以在 NavigationView 进行 Push 跳转隐藏底部的 Tabbar。我们只能在需要隐藏的界面的 onAppear和 onDisappear去隐藏。
/// ❌ 这样设置是不起作用的
UITabBar.appearance().isHidden = true
我们在运行时候,看一下布局。
我们按照结构找出 UITabbar
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 的方法
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
struct PalletBindBoxNumberPage: View {
...
var body: some View {
PageContentView(title: "托盘绑定箱号", viewModel: viewModel) {
...
}
.onAppear {
App.tabBar?.isHidden = false
}
.onDisappear {
App.tabBar?.isHidden = true
}
}
}
隐藏UITabar之后多出了很多空白的区域,我们设置忽略安全距离。
/// 页面的基础试图
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
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
struct PalletBindBoxNumberPage: View {
@StateObject private var viewModel = PalletBindBoxNumberPageViewModel()
var body: some View {
PageContentView(title: "托盘绑定箱号", viewModel: viewModel) {
EmptyView()
}
.makeToDetailPage()
}
}
修复第二次相同页面无法 Push 问题
从上面的掩饰发现,第一次是可以正常的进入,点击返回,第二次无法Push进入。只有点击其他页面返回之后,才能正常的返回。
打印点击 Push 对应的 ActionItem
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
struct HomePage: View {
...
var body: some View {
...
.onAppear {
...
viewModel.currentClickActionItem = nil
}
...
}
}
经过重置,第二次Push无法跳转问题解决了。
封装扫描输入组件
接下来我们封装上面的组件,大致的界面构造如下。
新建一个 ScanTextView
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()
}
}
添加栈版号和箱号
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()
}
}
...
}
}
class PalletBindBoxNumberPageViewModel: BaseViewModel {
/// 输入的栈版号
@Published var palletNumber:String = ""
/// 箱号
@Published var boxNumber:String = ""
}
固定 ScanTextView 的 Title 的宽度
提示语是没有对齐的,因为是自动布局,很难会让自动的对齐,我们需要设置左侧标题固定长度。
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)
...
}
...
}
}
栈版号和箱号中间添加分割线
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)
...
}
...
}
}
...
}
}
箱号详情组件
分析布局如下。
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)
}
}
制作标题信息组件
我们需要标题和信息上对齐,类似下面的排版方案。
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 组件
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")
}
}
...
}
}
调整上下组件的间距
struct BoxDetailView: View {
var body: some View {
HStack {
VStack {
...
Spacer()
.frame(height: 7.5)
...
}
VStack {
...
Spacer()
.frame(height: 7.5)
...
}
}
...
}
}
可手动控制 Title 的宽度
我们给TitleValueView新增一个可以手动控制Title宽度的参数,如果不为0则手动控制高度。
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))
}
}
我们将工单号和箱号宽度保持一致
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。
struct ScanTextView: View {
...
var body: some View {
HStack {
...
}
...
.frame(height:50)
}
}
只增加左右间距
高度50设置完毕,但是左右靠边,我们只设置边距左右为10。
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 参数箱号列表 用于更新列表
class PalletBindBoxNumberPageViewModel: BaseViewModel {
...
/// 箱子列表
@Published var boxDetailModels:[BoxDetailModel] = []
}
新增根据栈版号获取箱号列表方法
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这个扩展获取。
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()
}
}
...
}
...
}
}
...
}
}
添加或者删除箱号
此时我们的栈板上是没有数据的,需要我们输入箱号进行新增和删除操作。
上图的逻辑都封装在接口里面,所以我们只需要关心输入箱号之后,调用接口即可。
添加新增或者删除箱号逻辑方法
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方法
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
此时添加箱号成功了
{"code":200,"data":"ç®±å·ç»å®æ æ¿æå!!!","message":"success","objectType":null,"success":true}
在日志也看不出来乱码显示,我们希望提示给用户。
给获取箱子列表添加HUD
class PalletBindBoxNumberPageViewModel: BaseViewModel {
...
/// 请求获取箱子列表
func requestBoxDetailList() async {
...
let model:BaseModel<[BoxDetailModel]> = await request(api: api, showHUD: true)
...
}
...
}
此时我们已经获取到列表了,但是HUD没有消失,主要是逻辑中没有调用隐藏HUD。
给BaseViewModel新增Hidden HUD方法
@MainActor
class BaseViewModel: ObservableObject {
...
func hiddenHUD() {
self.isLoadingHUD = false
}
...
}
给查询箱号和新增和删除箱号添加HUD和移除HUD
class PalletBindBoxNumberPageViewModel: BaseViewModel {
...
/// 请求获取箱子列表
func requestBoxDetailList() async {
...
hiddenHUD()
...
}
/// 添加或者移除箱号
func addOrRemoveBox() async {
...
let model:BaseModel<String> = await request(api: api, showHUD: true)
...
hiddenHUD()
...
}
}
添加或者删除成功提示
上面的代码我们还是无法成功显示提示语,到底是添加成功还是删除成功。当我们请求完毕,展示获取的Data字符串。
但是展示和隐藏十分的快,在显示没有结束之前,被后面获取箱子列表接口在请求完毕之后隐藏了。
class PalletBindBoxNumberPageViewModel: BaseViewModel {
...
/// 添加或者移除箱号
func addOrRemoveBox() async {
...
if let message = model.data {
showHUDMessage(message: message)
}
}
}
修复HUD开始显示之前内容的问题
HUD展示逻辑
HUD Message展示逻辑
我们看到在展示文本延时两秒之后,文本没有清空,导致下次请求进行Loading时候因为文本不为空,展示不是一个Loading HUD而是上一个提示的文本。
清空上一个展示的文本
修复这个问题,大概有两种方案
方案1 在延时两秒隐藏时候 清空文本
@MainActor
class BaseViewModel: ObservableObject {
...
/// 展示 HUD 文本
/// - Parameter message: 提示的信息
func showHUDMessage(message:String) {
...
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
...
self.hudMessage = ""
}
}
...
}
方案2 在展示HUD的时候 清空之前的文本
@MainActor
class BaseViewModel: ObservableObject {
...
func request<T:Codable, API:APIConfig>(api:API, showHUD:Bool = false) async -> BaseModel<T> {
if (showHUD) {
hudMessage = ""
...
}
...
}
}
展示HUD Message的文本内容只是一个临时的展示内容,应该在展示完毕重置,所以第一种方案比较好。