第二十二章 onAppear|DataPickerView

获取当前工厂车间列表

这一章我们来给我的界面的数据写数据获取的实现和界面的交互。

对于显示当前选择的生产车间的,我们先是要获取到当前工厂可用的车间列表。

swift 复制代码
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)
            }
        }
    }
}
swift 复制代码
class AppConfig: ObservableObject {
    ...
    /// 选中的车间代码
    @AppStorage("workShopCode")
    var workShopCode:String?
}
swift 复制代码
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
    }
}

我们将车间的名称设置到界面上去。

swift 复制代码
struct MyPage: View {
    ...
    private func workshopCell() -> some View {
        MyDetailCellContentView(title: "车间", detail: viewModel.currentWorkShopName() ?? "请选择车间")
    }
    
   	...
}
shell 复制代码
{"success":false,"code":400,"message":"请选择工厂\n","data":null}

onAppear 请求数据

但是运行起来接口报错了,那说明我们Headers新增加的工厂的字段不存在。我们明明已经在我的页面获取工厂列表进行自动设置了,为啥还出现这种情况,经过分析问题处在下面代码。

swift 复制代码
class MyPageViewModel: BaseViewModel {
    ...
    override init() {
        super.init()
        Task {
            await getAllWorkShop()
        }
    }
    
    ...
}

我们MyPageViewModel初始化的时候就开始请求数据了,但是 MyPageViewModel 初始化和 MyPage一起初始化的,以为和首页刚初始化,就调用这个接口了。

我们修改一下调用的顺序。

swift 复制代码
class MyPageViewModel: BaseViewModel {
    ...
    public func initData() async {
        await getAllWorkShop()
    }
    
    ...
}
swift 复制代码
struct MyPage: View {
    ...
    var body: some View {
        PageContentView(title: "我的", viewModel: viewModel) {
            ...
        }
        .onAppear {
            Task {
                await viewModel.initData()
            }
        }
    }
    
    ...
}

在我的页面出现的时候再请求,我们就可以不会因为还没有设置工厂,导致接口报错。

刚才我们发现获取车间列表是默默请求的,为什么错误信息会被提示出来?而且提示信息还没完整的提示出来?

swift 复制代码
extension APIConfig {
    func request<M:Codable>(model:M.Type) async -> BaseModel<M> {
        do {
            ...
        } catch(let e) {
            ...
            return BaseModel(message: error.domain,
                             ...
        }
    }
}

错误提示没有是因为忘记把错误信息赋值导致的。

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

设置不展示HUD依然展示,是没有添加HUD的判断逻辑。

我们运行发现我的页面没有导航栏了,我们添加一个导航栏。

swift 复制代码
struct TabPage: View {
    ...
    var body: some View {
        TabView(selection:$currentTabIndex) {
            ...
            NavigationView {
                MyPage()
            }
               ...
        }
        ...
    }
}

有了导航条,但是和下面的内容串起来很难受,不过我们暂时先不管。我们发现一个大问题,就是我们的车间显示是下面的样子。

我们车间列表已经有数据,最少也是显示一条默认的,不可能存在请选择车间提示。研究了一下逻辑发现,当之前选择的车间在最新列表存在,就不会重新赋值,导致就无法通知进行更新。

swift 复制代码
class MyPageViewModel: BaseViewModel {
    ...
    private func getAllWorkShop() async {
        ...
        if let workShopCode = AppConfig.share.workShopCode {
            ...
            AppConfig.share.workShopCode = workShopCode
        } else {
            ...
        }
    }
    
    ...
}

Sheet 弹窗

我们默认选择的车间已经可以显示出来了,但是人工没法操作,接下来我们封装操作的弹框。

我们可以将视图分成下面组成方式。

我们只需要将蓝色区域底部对齐即可。

swift 复制代码
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)
    }
}

但是SwiftUIiOS的表现已经不是 UIDataPicker的样式了,可能为了为了支持全平台做了改变。那么Picker这个组件我们就不能用了,我们通过创建 UIPickerView进行转换。

swift 复制代码
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暂时就这样。

swift 复制代码
struct PickerSheet: View {
    ...
    var body: some View {
        VStack {
            ...
            DataPickerView(items: [
                "1",
                "2"
            ])
            ...
          	Spacer()
                .frame(height:20)
        }
        ...
    }
}

我们将 PickerSheet 组件进行提炼。

swift 复制代码
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

swift 复制代码
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))
    }
}

界面突然的出现有点不自然,我们添加一个从底部弹出的动画。

swift 复制代码
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 的标题和内容进行提炼。

swift 复制代码
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 弹出之后,没有办法进行消失。我们新增可以消失的方法。

swift 复制代码
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)
                        
                }
                ...
            }
        }
    }
}
swift 复制代码
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: {
                   	...
                }
            }
            ...
        }
        ...
    }
}

我们来实现一下点击车间,弹出所有可以选择的车间列表。

swift 复制代码
extension GetAllWorkshopResponse: DataPickerItem {
    var pickerItemTitle: String {name ?? ""}
}
swift 复制代码
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之前的一套做出来呢?

相关推荐
君赏4 小时前
第二十三章 UIHostingController|withAnimation|SwiftUI 默认动画时间
swiftui
君赏4 小时前
第二十四章 init 方法初始化 State
swiftui
君赏4 小时前
第十九章 TabView|accentColor|AnyView|NavigationView|navigationTitle|navigationBarTit
swiftui
君赏4 小时前
第十七章 @MainActor
swiftui
君赏4 小时前
第十六章 RoundedRectangle|aspectRatio|UIViewRepresentable
swiftui
君赏4 小时前
第十八章 封装HUD和完善登录界面逻辑
swiftui
君赏4 小时前
第十五章 Task|NSAppTransportSecurity|keyDecodingStrategy
swiftui
君赏4 小时前
第十四章 async/await|overlay|PreferencrKey|Anchor
swiftui
君赏4 小时前
第十三章 Button|cornerRadius
swiftui