用户登录之后,就可以进入首页了,我们看一下首页的 UI的样子。
我们先创建一个 HomePage。
我们在入口修改逻辑,支持登录完毕进入首页。
TabView 创建 TabBar
我们登录完毕,或者下次启动就进入了首页了。我们首页底部是有 Tab 的,我们需要用到 TabView。
我们创建一个 TabPage,用户显示我们首页底部的 Tab。
我们修改一下代码,将我们的 HomePage 添加进去。
这显示的效果明显不是我们需要的效果,而且文本怎么变成蓝色了?我们需要的是下面的效果
accentColor设置 TabBar 选中颜色
我们尝试一下设置一下文本的前景色。
但是没有任何的效果,这时候我们需要谷歌一下资料是什么原因了。发现网上一些文章答案已经不能用了,但是 accentColor 这个还是有效果的,但就是废弃了。
需要用最新的 tint(_:),如果想更改默认未选中 item 的颜色,需要通过下面代码设置。
尝试封装 TabView
为了我们的 TabItem 可以方便的进行设置,我们决定封装一下我们 TabView。
我们如果封装,需要用户提供试图内容,未选中的图标,选中图标,选中的颜色,未选中的颜色,还有当前选中的索引。
我们需要用户传入一个 TabItem 的数组,我们通过数组进行创建 TabView的item。
但是我们的范型的结构体无法放在数组里面。那么,我们可以将范型设置为 AnyView,这样报错解决了,但是在SwiftUI中最好不要用到 AnyView,这会导致系统无法推断最外层结构,从而无法优化Diff算法,优化性能。
走到这一步,我们发现还是不要封装为好,毕竟超过5个的tabItem就已经少之又少。
我们重新修改一下 TabPage的代码。
⚠️我们使用最新的 .tint(\_:) 会经常不起效果,但是换成 .accentColor(_:)就可以。
对于 TabView 我就先到此为止了,目前也是达到我们的效果,接下来我们开始做我们首页的逻辑。
NavigationView 使用导航
首页的头部是一个导航条,并且左侧有一个进行选择的选择框。对于导航,我们需要用到 NavigationView。
.navigationTitle 设置导航标题
但是我们怎么设置导航标题呢?我们可以在任何子组件通过 .navigationTitle进行设置。
.navigationBarTitleDisplayMode 设置导航样式
但是我们的导航显示是默认的大标题,是符合 iOS新版本的系统风格一样。不过我们可以通过.navigationBarTitleDisplayMode进行设置导航标题的显示模式。
.toolbar 添加导航按钮
此时我们的导航的标题已经显示正常了。但是我们工厂选择的组件怎么添加到首页左侧的位置呢?经过谷歌之后,我们发现可以通过.toolbar的方法轻松的添加左侧和右侧的视图。
我们将添加导航的代码提炼出来,并且设置页面背景颜色为淡灰色。
struct HomePage: View {
@StateObject private var appColor:AppColor = AppColor.share
var body: some View {
NavigationView {
navigationBar {
Color(uiColor: appColor.c_fefefe)
}
}
}
private func navigationBar<Content:View>(@ViewBuilder content:() -> Content) -> some View {
content()
.navigationTitle(Text("首页"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement:.navigationBarLeading) {
HStack(spacing:6) {
Text("请选择工厂")
.foregroundColor(Color(uiColor: appColor.c_999999))
.font(.system(size: 15))
Image("drop_icon")
}
}
}
}
}
首页进来需要先获取工厂列表,如果之前设置过并且不在工厂列表,或者都没设置过工厂,则默认为列表的第一个工厂。
我们不管怎么样的逻辑,第一步就是获取工厂列表,之后才能做剩下的逻辑。
class HomePageViewModel: BaseViewModel {
/// 请求工厂列表
func requestFactoryList() async {
let api = FactoryListApi()
let model:BaseModel<FactoryListResponseModel> = await request(api: api)
// 下面逻辑
}
}
现在我们就拿到了全部的工厂列表,我们判断请求成功就保存工厂列表到当前页面。
// 新建一个 @Published 可以接受请求的工厂列表并通知更新
/// 工厂列表
@Published var factoryList:[FactoryListResponseModel] = []
// 请求成功就将工厂数据更新
guard model._isSuccess else {return}
factoryList = model.data ?? []
Hashable 解决 ForEach 可能 Sting 相同报错
有了工厂列表我们就可以点击 PopMenuButton 展示所有的工厂列表了。但是我们渲染的时候出现了报错,提示下面的报错。
ForEach<Array<String>, String, ModifiedContent<ModifiedContent<PopMenuButtonItem, _BackgroundStyleModifier<BackgroundStyle>>, AddGestureModifier<_EndedGesture<TapGesture>>>>: the ID 111111 occurs multiple times within the collection, this will give undefined results!
提示我们多次出现了数据111111,可能会导致找不到唯一 ID的结果。这样一看,确实是我们当初封装的时候考虑的太浅,没有想到展示的数据可能名字一样,虽然不合理,但是存在。
为了解决这个问题,我们必须对 PopMenuButton 进行重构,我们需要将数据假设成一个协议。
protocol PopMenuItem:Hashable {
/// 显示在 Menu Item 的文字
var menuTitle:String {get}
}
/// old
struct PopMenuButton: View {
let items:[String]
@Binding var currentItem:String
/// new
struct PopMenuButton<T:PopMenuItem>: View {
let items:[T]
@Binding var currentItem:T
/// old
typealias ItemValueChanged = (String) -> Void
/// new
typealias ItemValueChanged = (T) -> Void
/// old
PopMenuButtonItem(title: item,
/// new
PopMenuButtonItem(title: item.menuTitle,
我们将PopMenButton 的代码修改成如上。但是之前的示例和引用都会报错,对于名字重复出现几率很小,不可能不允许纯文本数组支持。
String 实现 PopMenuItem 实现兼容
为了兼容和支持纯文本数组的支持,我们新增String的扩展。
extension String: PopMenuItem {
var menuTitle: String {self}
}
为了修复我们工厂数据源,因为工厂名字存在重复,我们将 FactoryListResponseModel 实现我们 PopMenuItem 协议。
extension FactoryListResponseModel: PopMenuItem {
var menuTitle: String { factoryName ?? "" }
/// 重写 == 方法 为了自定义实现两个模型是否一样
static func ==(lhs:FactoryListResponseModel, rhs:FactoryListResponseModel) -> Bool {
guard let leftCode = lhs.factoryCode, let rightCode = rhs.factoryCode else {return false}
return leftCode == rightCode
}
}
我们调整一下首页的代码,来支持 FactoryListResponseModel 模型。
/// HomePageViewMode
/// old
@Published var currentFactoryName:String = ""
/// new
@Published var currentFactory:FactoryListResponseModel = FactoryListResponseModel(factoryCode: nil,
factoryName: nil)
PopMenuButtonModify
/// old
struct PopMenuButtonModify: ViewModifier {
let items:[String]
@Binding var currentItem:String
/// new
struct PopMenuButtonModify<T:PopMenuItem>: ViewModifier {
let items:[T]
@Binding var currentItem:T
View+popMenuButton
/// old
func popMenuButton(items:[String],
currentItem:Binding<String>,
isShowPopMenuButton:Binding<Bool>) -> some View{
/// new
func popMenuButton<T:PopMenuItem>(items:[T],
currentItem:Binding<T>,
isShowPopMenuButton:Binding<Bool>) -> some View{
HomePage
/// old
.popMenuButton(items: factoryNames,
currentItem: $viewModel.currentFactoryName,
/// new
.popMenuButton(items: viewModel.factoryList,
currentItem: $viewModel.currentFactory,
此时我们工厂选择再也不报错误了,可以正常的显示出来了。