获取当前工厂车间列表
这一章我们来给我的界面的数据写数据获取的实现和界面的交互。
对于显示当前选择的生产车间的,我们先是要获取到当前工厂可用的车间列表。
class Api: API {
...
static var defaultHeadersConfig: ((inout HTTPHeaders) -> Void)? {
return { headers in
...
if let currentFactoryCode = AppConfig.share.currentFactoryCode {
headers.add(name: "x-winplus-factory-code", value: currentFactoryCode)
}
}
}
}
class AppConfig: ObservableObject {
...
/// 选中的车间代码
@AppStorage("workShopCode")
var workShopCode:String?
}
class MyPageViewModel: BaseViewModel {
/// 工厂所有的车间列表
private var workShops:[GetAllWorkshopResponse] = []
override init() {
super.init()
Task {
await getAllWorkShop()
}
}
private func getAllWorkShop() async {
let api = GetAllWorkshopApi()
let model:BaseModel<[GetAllWorkshopResponse]> = await request(api: api, showHUD: false)
guard model._isSuccess else {return}
guard let data = model.data else {return}
workShops = data
if let workShopCode = AppConfig.share.workShopCode {
let index = workShops.firstIndex { response in
guard let _workshopCode = response.workshopCode else {return false}
return workShopCode == _workshopCode
}
guard let _ = index else {
/// 如果查询不到 意味着之前选中的数据已经不存在 则默认第一个
AppConfig.share.workShopCode = workShops.first?.workshopCode
return
}
} else {
/// 如果之前没有选中的车间 则默认第一个
AppConfig.share.workShopCode = workShops.first?.workshopCode
}
}
/// 当前选中车间的名称
func currentWorkShopName() -> String? {
return workShops.first { response in
guard let workShopCode = AppConfig.share.workShopCode else {return false}
guard let _workShopCode = response.workshopCode else {return false}
return workShopCode == _workShopCode
}?.name
}
}
我们将车间的名称设置到界面上去。
struct MyPage: View {
...
private func workshopCell() -> some View {
MyDetailCellContentView(title: "车间", detail: viewModel.currentWorkShopName() ?? "请选择车间")
}
...
}
{"success":false,"code":400,"message":"请选择工厂\n","data":null}
onAppear 请求数据
但是运行起来接口报错了,那说明我们Headers新增加的工厂的字段不存在。我们明明已经在我的页面获取工厂列表进行自动设置了,为啥还出现这种情况,经过分析问题处在下面代码。
class MyPageViewModel: BaseViewModel {
...
override init() {
super.init()
Task {
await getAllWorkShop()
}
}
...
}
我们MyPageViewModel初始化的时候就开始请求数据了,但是 MyPageViewModel 初始化和 MyPage一起初始化的,以为和首页刚初始化,就调用这个接口了。
我们修改一下调用的顺序。
class MyPageViewModel: BaseViewModel {
...
public func initData() async {
await getAllWorkShop()
}
...
}
struct MyPage: View {
...
var body: some View {
PageContentView(title: "我的", viewModel: viewModel) {
...
}
.onAppear {
Task {
await viewModel.initData()
}
}
}
...
}
在我的页面出现的时候再请求,我们就可以不会因为还没有设置工厂,导致接口报错。
刚才我们发现获取车间列表是默默请求的,为什么错误信息会被提示出来?而且提示信息还没完整的提示出来?
extension APIConfig {
func request<M:Codable>(model:M.Type) async -> BaseModel<M> {
do {
...
} catch(let e) {
...
return BaseModel(message: error.domain,
...
}
}
}
错误提示没有是因为忘记把错误信息赋值导致的。
@MainActor
class BaseViewModel: ObservableObject {
...
func request<T:Codable, API:APIConfig>(api:API, showHUD:Bool = false) async -> BaseModel<T> {
...
if (!model._isSuccess && showHUD) {
...
}
...
}
}
设置不展示HUD依然展示,是没有添加HUD的判断逻辑。
我们运行发现我的页面没有导航栏了,我们添加一个导航栏。
struct TabPage: View {
...
var body: some View {
TabView(selection:$currentTabIndex) {
...
NavigationView {
MyPage()
}
...
}
...
}
}
有了导航条,但是和下面的内容串起来很难受,不过我们暂时先不管。我们发现一个大问题,就是我们的车间显示是下面的样子。
我们车间列表已经有数据,最少也是显示一条默认的,不可能存在请选择车间提示。研究了一下逻辑发现,当之前选择的车间在最新列表存在,就不会重新赋值,导致就无法通知进行更新。
class MyPageViewModel: BaseViewModel {
...
private func getAllWorkShop() async {
...
if let workShopCode = AppConfig.share.workShopCode {
...
AppConfig.share.workShopCode = workShopCode
} else {
...
}
}
...
}
Sheet 弹窗
我们默认选择的车间已经可以显示出来了,但是人工没法操作,接下来我们封装操作的弹框。
我们可以将视图分成下面组成方式。
我们只需要将蓝色区域底部对齐即可。
struct PickerSheet: View {
@StateObject private var appColor = AppColor.share
var body: some View {
VStack {
Text("车间选择")
.foregroundColor(Color(uiColor: appColor.c_333333))
.font(.system(size: 16))
.padding(EdgeInsets(top: 20, leading: 0, bottom: 20, trailing: 0))
Rectangle()
.frame(height:0.5)
.foregroundColor(Color(appColor.c_d8d8d8))
Picker(selection: .constant(1), label: Text("Picker")) {
Text("1").tag(1)
Text("2").tag(2)
}
HStack {
Button {
} label: {
Text("取消")
.font(.system(size: 14))
.foregroundColor(Color(uiColor: appColor.c_999999))
.padding(EdgeInsets(top: 10, leading: 50, bottom: 10, trailing: 50))
.overlay {
RoundedRectangle(cornerSize: CGSize(width: 5, height: 5))
.stroke(Color(uiColor: appColor.c_999999),lineWidth: 0.5)
}
}
Button {
} label: {
Text("确定")
.font(.system(size: 14))
.foregroundColor(.white)
.padding(EdgeInsets(top: 10, leading: 50, bottom: 10, trailing: 50))
.background(Color(uiColor:appColor.c_209090))
.cornerRadius(5)
}
}
}
.frame(maxWidth: .infinity)
}
}
但是SwiftUI在iOS的表现已经不是 UIDataPicker的样式了,可能为了为了支持全平台做了改变。那么Picker这个组件我们就不能用了,我们通过创建 UIPickerView进行转换。
struct DataPickerView<Item:DataPickerItem>: UIViewRepresentable {
typealias UIViewType = UIPickerView
private let items:[Item]
init(items:[Item]) {
self.items = items
}
func makeCoordinator() -> DataPickerViewCoordinator<Item> {
return DataPickerViewCoordinator(items: items)
}
func makeUIView(context: Context) -> UIPickerView {
let picker = UIPickerView()
picker.dataSource = context.coordinator
picker.delegate = context.coordinator
let v = UIView()
v.backgroundColor = .red
picker.addSubview(v)
return picker
}
func updateUIView(_ uiView: UIPickerView, context: Context) {
}
}
class DataPickerViewCoordinator<Item:DataPickerItem>: NSObject, UIPickerViewDataSource, UIPickerViewDelegate {
private let items:[Item]
init(items:[Item]) {
self.items = items
}
func numberOfComponents(in pickerView: UIPickerView) -> Int {
1
}
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
items.count
}
func pickerView(_ pickerView: UIPickerView, viewForRow row: Int, forComponent component: Int, reusing view: UIView?) -> UIView {
let contentView = rootView(row: row)
return UIHostingController(rootView: contentView).view
}
func pickerView(_ pickerView: UIPickerView, rowHeightForComponent component: Int) -> CGFloat {
50
}
private func rootView(row:Int) -> some View {
VStack {
Rectangle()
.frame(height:0.5)
.foregroundColor(Color(uiColor: AppColor.share.c_209090))
Spacer()
Text(items[row].pickerItemTitle)
.font(.system(size: 12))
.foregroundColor(Color(uiColor: AppColor.share.c_209090))
Spacer()
Rectangle()
.frame(height:0.5)
.foregroundColor(Color(uiColor: AppColor.share.c_209090))
}
}
}
protocol DataPickerItem {
var pickerItemTitle:String {get}
}
extension String: DataPickerItem {
var pickerItemTitle: String {self}
}
选中默认的灰色的遮罩暂时没有找到可以修改的方法,对于DataPicker暂时就这样。
struct PickerSheet: View {
...
var body: some View {
VStack {
...
DataPickerView(items: [
"1",
"2"
])
...
Spacer()
.frame(height:20)
}
...
}
}
我们将 PickerSheet 组件进行提炼。
struct PickerSheet<Item:DataPickerItem>: View {
...
private let title:String
private let items:[Item]
init(title:String, items:[Item]) {
self.title = title
self.items = items
}
var body: some View {
VStack {
Text(title)
...
...
DataPickerView(items: items)
...
}
...
}
}
为了可以方便调用,我们封装一个 ViewModify
struct DataPickerViewModify: ViewModifier {
@Binding var isShow:Bool
func body(content: Content) -> some View {
ZStack {
content
if isShow {
GeometryReader { geometry in
VStack {
Spacer()
PickerSheet(title: "仓库",
items: [
"1",
"2"
])
.background(.white)
}
}
.background(Color(uiColor: UIColor.black.withAlphaComponent(0.6)))
}
}
}
}
extension View {
func dataPicker(isShow:Binding<Bool>) -> some View {
self.modifier(DataPickerViewModify(isShow: isShow))
}
}
界面突然的出现有点不自然,我们添加一个从底部弹出的动画。
struct DataPickerViewModify: ViewModifier {
...
func body(content: Content) -> some View {
ZStack {
content
if isShow {
Color(uiColor: UIColor.black.withAlphaComponent(0.6))
.edgesIgnoringSafeArea(.all)
VStack {
Spacer()
PickerSheet(...)
}
.transition(.move(edge: .bottom))
.animation(.linear)
}
}
}
}
我们将 DataPickerViewModify 的标题和内容进行提炼。
struct DataPickerViewModify<Item:DataPickerItem>: ViewModifier {
...
private var title:String
private var items:[Item]
init(title:String, items:[Item], isShow:Binding<Bool>) {
self.title = title
self.items = items
self._isShow = isShow
}
func body(content: Content) -> some View {
ZStack {
...
if isShow {
...
VStack {
...
PickerSheet(title: title,
items: items)
...
}
...
}
}
}
}
此时我们的 DataPicker 弹出之后,没有办法进行消失。我们新增可以消失的方法。
struct DataPickerViewModify<Item:DataPickerItem>: ViewModifier {
...
func body(content: Content) -> some View {
ZStack {
...
if isShow {
Color(uiColor: UIColor.black.withAlphaComponent(0.6))
...
.onTapGesture {
isShow = false
}
VStack {
...
PickerSheet(title: title,
items: items,
isShow: $isShow)
.background(.white)
}
...
}
}
}
}
struct PickerSheet<Item:DataPickerItem>: View {
...
@Binding private var isShow:Bool
...
init(title:String, items:[Item], isShow:Binding<Bool>) {
...
self._isShow = isShow
}
var body: some View {
VStack {
...
HStack {
Button {
isShow = false
} label: {
...
}
Button {
isShow = false
} label: {
...
}
}
...
}
...
}
}
我们来实现一下点击车间,弹出所有可以选择的车间列表。
extension GetAllWorkshopResponse: DataPickerItem {
var pickerItemTitle: String {name ?? ""}
}
struct MyPage: View {
...
var body: some View {
PageContentView(title: "我的", viewModel: viewModel) {
VStack(spacing: 0) {
...
workshopCell()
.onTapGesture {
viewModel.isShowDataPicker.toggle()
}
...
}
}
...
.dataPicker(title: "车间",
items: viewModel.workShops,
isShow: $viewModel.isShowDataPicker)
}
...
}
我们发现我们的弹出试图被上层的TabPage遮挡了。如果想要在最外层。那么这个控件就要注入在最外层才可以。但是数据怎么传递到最外层呢?
这个方案实现起来难度很大,我们用系统的 fullScreenCover 来实现,但是最终的效果是下面的效果。
黑色透明背景也是一起跟着动的,就这一点不满足我们的需求。我们能否通过 UIViewController之前的一套做出来呢?