iOS swift Gitee客户端 完整项目分析之二

1 项目地址

https://gitee.com/open-gitee/gitee_ios
Gitee基于SwiftUI和OpenApi的iOS客户端项目。

2 首页其他列表

2.1 Issues

点击Issues这个item后,会跳转到所有Issures的列表页:

struct IssuesView: View {
    @State var issuesList: [IssueModel] = []
    @State var waitPlease = false
    @State var isRefreshing = false
    @State var isLoading: Bool = false
    @State var isModal: Bool = false
    @State var message: String = "数据加载中"
    @State var isFilterShow = false
    @State var page = 1
    @State var repoPath: String = ""
    @State var title: String? = "Issues"
    @State var isInserIssueShow:Bool = false

首先继承View,声明动态数据,因为swift是静态语言,这里通过注解@State实现动态监听。

@State 是 SwiftUI 中的一个注解,它用于声明一个状态变量。SwiftUI 框架在渲染界面时会自动监测状态变量的变化,并且自动更新对应的界面元素。这样可以方便地管理界面状态和数据。

然后是body声明:

var body: some View {
    LoadingView(isLoading:self.$isLoading,message: self.$message,isModal:self.$isModal) {
        VStack{
            ZStack{
                if issuesList.count == 0 && !isLoading {
                    VStack{
                        Image(systemName: "doc.text.magnifyingglass")
                            .scaleEffect(3, anchor: .center)
                        Text("暂无查询到的Issues").padding(.top,30)
                    }
                }
                RefreshView(refreshing: $isRefreshing, action: {
                    self.page = 1
                    self.getIssueList(page: self.page)
                }) {
                    LazyVStack{
                        ForEach(self.issuesList){ item in
                            NavigationLink(destination: IssueItemView(issueItem: item)) {
                                IssueItemView(issueItem: item)
                                    .onAppear(){
                                        if !waitPlease && item.id == issuesList[issuesList.count - 1].id {
                                            self.page = self.page + 1
                                            self.getIssueList(page: self.page)
                                        }
                                    }
                            }
                        }
                    }
                }
            }
            if self.repoPath != "" {
                Spacer()
                HStack{
                    Spacer()
                    Button {
                        self.isInserIssueShow = true
                    } label: {
                        Image(systemName: "square.and.pencil")
                            .scaleEffect(1.2, anchor: .center)
                    }
                    .padding(12)
                    .background(Color.yellow)
                    .foregroundColor(Color.black)
                    .cornerRadius(100)
                    .sheet(isPresented: $isInserIssueShow) {
                        let arr = self.repoPath.components(separatedBy: "/")
                        IssueInsertView(repoNamespacePath: arr[0], repoPath: arr[1])
                    }
                }
                .padding(.trailing,20)
            }
        }
    }
    .padding(.top,5)
    .navigationBarTitle(Text(title!), displayMode: .inline)
    .navigationBarItems(
        trailing:
            HStack {
                Button(action: {
                    self.isFilterShow.toggle()
                }) {
                    Image(systemName: "rectangle.and.text.magnifyingglass")
                        
                        .scaleEffect(1, anchor: .center)
                }
            }
            .sheet(isPresented: $isFilterShow,onDismiss: {
                self.isLoading = true
                self.page = 1
                self.getIssueList(page: self.page)
            }){
                IssueFilterView()
                    .modifier(DisableModalDismiss(disabled: false))
            }
    )
    .onAppear(){
        localConfig.setValue("all", forKey: giteeConfig.issue_filter)
        localConfig.setValue("open", forKey: giteeConfig.issue_state)
        localConfig.setValue("created", forKey: giteeConfig.issue_sort)
        localConfig.setValue("desc", forKey: giteeConfig.issue_direction)
        self.page = 1
        self.getIssueList(page: self.page)
    }
}

上面是定义了整个Issues列表的UI。
内部用了自定义的下拉刷新控件:RefreshView。
然后里面是通过一个ForEach循环,里面装载了IssueItemView,就是单个item布局效果。

这个跟前面分析的IssueItemView一致。

这里Issue页面,标题栏右边有个筛选按钮:

.navigationBarItems(
        trailing:
            HStack {
                Button(action: {
                    self.isFilterShow.toggle()
                }) {
                    Image(systemName: "rectangle.and.text.magnifyingglass")
                        
                        .scaleEffect(1, anchor: .center)
                }
            }
            .sheet(isPresented: $isFilterShow,onDismiss: {
                self.isLoading = true
                self.page = 1
                self.getIssueList(page: self.page)
            }){
                IssueFilterView()
                    .modifier(DisableModalDismiss(disabled: false))
            }
    )

是通过给View扩展,添加trailing尾部,配置图标,以及点击后的响应view: IssureFilterView。

这里看下筛选View:

对应的代码如下:


struct IssueFilterView: View {
    @Environment(\.presentationMode) var mode
    @State var filter = 0;
    @State var filterList = ["所有来源的Issues","只显示指派给我的","只显示我创建的"];
    @State var filterKey = ["all","assigned","created"];
    @State var state = 1;
    @State var stateList = ["所有状态的Issues","只显示已开启的","只显示进行中的","只显示已完成的","只显示被拒绝的"];
    @State var stateKey = ["all","open","progressing","closed","rejected"];
    @State var sort = 0;
    @State var sortList = ["创建时间","更新时间"];
    @State var sortKey = ["created","updated"];
    @State var direction = 0;
    @State var directionList = ["从最新开始","从最早开始"];
    @State var directionKey = ["desc","asc"];
    var body: some View {
        NavigationView{
            Form{
                Section(header: Text("条件")) {
                    Picker(selection: self.$filter, label: Text("Issues来源")) {
                        ForEach(0 ..< self.filterList.count){
                            Text(self.filterList[$0]).tag($0)
                        }
                    }
                    Picker(selection: self.$state, label: Text("当前状态")) {
                        ForEach(0 ..< self.stateList.count){
                            Text(self.stateList[$0]).tag($0)
                        }
                    }
                }
                Section(header: Text("排序")) {
                    Picker(selection: self.$sort, label: Text("排序依据")) {
                        ForEach(0 ..< self.sortList.count){
                            Text(self.sortList[$0]).tag($0)
                        }
                    }
                    Picker(selection: self.$direction, label: Text("排序方式")) {
                        ForEach(0 ..< self.directionList.count){
                            Text(self.directionList[$0]).tag($0)
                        }
                    }
                }
            }
            .navigationBarTitle(Text("筛选Issues"), displayMode: .large)
            .navigationBarItems(trailing:
                                    Button(action: {
                                        // 存储这部分配置
                                        localConfig.setValue(self.filterKey[self.filter], forKey: giteeConfig.issue_filter)
                                        localConfig.setValue(self.stateKey[self.state], forKey: giteeConfig.issue_state)
                                        localConfig.setValue(self.sortKey[self.sort], forKey: giteeConfig.issue_sort)
                                        localConfig.setValue(self.directionKey[self.direction], forKey: giteeConfig.issue_direction)
                                        self.mode.wrappedValue.dismiss()
                                    }) {
                                        Text("筛选").foregroundColor(.yellow)
                                    })
        }
        .onAppear(){
            
        }
        .preferredColorScheme(.dark)
    }
}

这里是跳转到一个系统的Picker视图页:

然后是右侧按钮点击事件:

 .navigationBarItems(trailing:
        Button(action: {
            // 存储这部分配置
            localConfig.setValue(self.filterKey[self.filter], forKey: giteeConfig.issue_filter)
            localConfig.setValue(self.stateKey[self.state], forKey: giteeConfig.issue_state)
            localConfig.setValue(self.sortKey[self.sort], forKey: giteeConfig.issue_sort)
            localConfig.setValue(self.directionKey[self.direction], forKey: giteeConfig.issue_direction)
            self.mode.wrappedValue.dismiss()
        }) {
            Text("筛选").foregroundColor(.yellow)
        })

点击后,先存储配置,然后怎么通知列表更新的呢?

应该是这里:

.navigationBarItems(
        trailing:
            HStack {
                Button(action: {
                    self.isFilterShow.toggle()
                }) {
                    Image(systemName: "rectangle.and.text.magnifyingglass")
                        
                        .scaleEffect(1, anchor: .center)
                }
            }
            .sheet(isPresented: $isFilterShow,onDismiss: {
                self.isLoading = true
                self.page = 1
                self.getIssueList(page: self.page)
            }){
                IssueFilterView()
                    .modifier(DisableModalDismiss(disabled: false))
            }
    )

通过配置sheet方法,在里面的view发送dissmiss后,会触发sheet里面的onDismiss回调,这里就重新走接口了。走接口的话应该会重新拿localConfig里面的值。

sheet 方法是 SwiftUI 中的一个视图方法,用于从当前视图弹出一个新的视图作为模态视图。模态视图是一种浮现在当前界面之上的视图,通常用于呈现额外的内容或提供选项等。模态视图不会阻碍用户对当前界面的操作,但必须关闭模态视图才能继续对当前界面的操作。

然后就是如何获取网络层数据的?

.onAppear(){
        print("这里appear了")
        localConfig.setValue("all", forKey: giteeConfig.issue_filter)
        localConfig.setValue("open", forKey: giteeConfig.issue_state)
        localConfig.setValue("created", forKey: giteeConfig.issue_sort)
        localConfig.setValue("desc", forKey: giteeConfig.issue_direction)
        self.page = 1
        self.getIssueList(page: self.page)
    }

主要还是这里,在onAppear的时候,去拿接口数据了。

func getIssueList(page: Int){
    if self.waitPlease { return }
    self.waitPlease = true
    if issuesList.count == 0 {
        self.isLoading = true
    }
    let state = localConfig.string(forKey: giteeConfig.issue_state)
    let filter = localConfig.string(forKey: giteeConfig.issue_filter)
    let direction = localConfig.string(forKey: giteeConfig.issue_direction)
    let sort = localConfig.string(forKey: giteeConfig.issue_sort)
    var url = "user/issues?page=" + String(page)
    
    if self.repoPath != "" {
        url = "repos/" + self.repoPath + "/issues?page=" + String(page)
    }
    
    url = url + "&state=" + state!
    url = url + "&filter=" + filter!
    url = url + "&direction=" + direction!
    url = url + "&sort=" + sort!
    
    
    HttpRequest(url: url, withAccessToken: true)
        .doGet { (value) in
            let json = JSON(value)
            if json["message"].string != nil {
                print("error")
                DispatchQueue.main.async {
                    UIAlertController.confirm(message: json["message"].stringValue, title: "发生错误", confirmText: "重新登录", cancelText: "返回") { (action) in
                        Helper.relogin()
                    }
                }
            }else{
                var tempList = self.issuesList
                if page == 1{
                    tempList = []
                }
                for (_,subJson):(String, JSON) in json {
                    let repoNamespace = RepoNamespace(id: Int(subJson["repository"]["namespace"]["id"].stringValue)!, name: String(subJson["repository"]["namespace"]["name"].stringValue), path:  String(subJson["repository"]["namespace"]["path"].stringValue))
                    let repoInfo = RepoModel(id: Int(subJson["repository"]["id"].stringValue)!, repoName: String(subJson["repository"]["name"].stringValue),repoPath:  String(subJson["repository"]["path"].stringValue),repoNamespace: repoNamespace, repoDesc:  String(subJson["repository"]["description"].stringValue), repoForks:  String(subJson["repository"]["forks_count"].stringValue), repoStars:  String(subJson["repository"]["stargazers_count"].stringValue), repoWatches:  String(subJson["repository"]["watchers_count"].stringValue), repoLicense:  String(subJson["repository"]["license"].stringValue), repoLanguage:  String(subJson["repository"]["language"].stringValue), repoPushDate:  String(subJson["repository"]["pushed_at"].stringValue), repoIsFork:  Bool(subJson["repository"]["fork"].boolValue), repoIsOpenSource:  Bool(subJson["repository"]["public"].boolValue), repoIssues:  String(subJson["repository"]["open_issues_count"].stringValue), repoDefaultBranch:  String(subJson["repository"]["default_branch"].stringValue))
                    let userInfo = UserItemModel(id: Int(subJson["user"]["id"].stringValue)!, userHead: String(subJson["user"]["avatar_url"].stringValue), userName: String(subJson["user"]["name"].stringValue), userAccount: String(subJson["user"]["login"].stringValue))
                    let issueInfo = IssueModel(id: Int(subJson["id"].stringValue)!, issueId: String(subJson["number"].stringValue), issueTitle: String(subJson["title"].stringValue), issueTime: Helper.getDateFromString(str: String(subJson["created_at"].stringValue)), issueDesc: String(subJson["body"].stringValue), issueStatus: getIssueStatus(status: subJson["state"].stringValue), repoInfo:repoInfo, userInfo: userInfo)
                    tempList.append(
                        issueInfo
                    )
                }
                self.issuesList = tempList
            }
            self.isRefreshing = false
            self.isLoading = false
            self.waitPlease = false
        } errorCallback: {
            self.isRefreshing = false
            self.isLoading = false
            self.waitPlease = false
            
        }
}

这里利用localConfig拿到了存储的筛选数据,通过HttpRequest实现获取数据。

2.2 你的私信

Section(header: Text("消息中心")) {
            NavigationLink(destination: MailView()) {
                HomeListItem(title: "你的私信",icon:"envelope.circle", color: Color(hex: 0x16c3fc))
            }
            NavigationLink(destination: NotificationView()) {
                HomeListItem(title: "你的通知",icon:"bell.circle", color: Color(hex: 0xf83860))
            }
        }

这里是首页的入口。

点击你的私信会跳转到MailView页面。
首先看下声明的状态变量:

struct MailView: View {
    @State var mailList: [MailModel] = []
    @State var waitPlease = false
    @State var isRefreshing = false
    @State var isLoading: Bool = false
    @State var isModal: Bool = false
    @State var message: String = "数据加载中"
    @State var page = 1

然后是body声明:

var body: some View {
    LoadingView(isLoading:self.$isLoading,message: self.$message,isModal:self.$isModal) {
        ZStack{
            if mailList.count == 0 && !isLoading {
                VStack{
                    Image(systemName: "doc.text.magnifyingglass")
                        .scaleEffect(3, anchor: .center)
                    Text("暂无查询到的私信").padding(.top,30)
                }
            }
            RefreshView(refreshing: $isRefreshing, action: {
                self.page = 1
                self.getMailList(page: self.page)
            }) {
                LazyVStack{
                    ForEach(self.mailList){ item in
                        MailItemView(mailItem: item)
                            .onAppear(){
                                if !waitPlease && item.id == mailList[mailList.count - 1].id {
                                    self.page = self.page + 1
                                    self.getMailList(page: self.page)
                                }
                            }
                    }
                }
            }
        }
    }
    .padding(.top,5)
    .navigationBarTitle(Text("你的私信"), displayMode: .inline)
    .onAppear(){
        self.page = 1
        self.getMailList(page: self.page)
    }
}

也是一个LoadingView负责加载loading的封装视图,然后是一个刷新RefreshView。
里面是循环的item。
可见的时候走getMailList接口。

先看下item吧:

struct MailItemView:View{
    @State var mailItem: MailModel
    @State var placeholderImage = UIImage(named: "Logo")!
    @State var isMessageReplyShow:Bool = false
    var body: some View{
        ZStack{
            VStack{

            }
            .frame(width: 0, height: 0)
            .opacity(0)
            
            VStack{
                HStack(alignment: .top) {
                    Image(uiImage: placeholderImage)
                        .resizable()
                        .scaledToFit()
                        .frame(
                            width:40,height:40,
                            alignment: .center
                        )
                        .cornerRadius(5)
                        .onAppear(){
                            guard let url = URL(string: mailItem.user.userHead) else {
                                return
                            }
                            URLSession.shared.dataTask(with: url) { (data, response, error) in
                                if let data = data, let image = UIImage(data: data) {
                                    placeholderImage = image
                                }
                            }.resume()
                        }
                    VStack(alignment: .leading){
                        HStack(alignment:.top){
                            Text(mailItem.user.userName)
                                .padding(0)
                            Spacer()
                            Text(mailItem.time).font(.system(size:12)).foregroundColor(.gray)
                        }
                        Text(mailItem.message).font(.system(size:14)).foregroundColor(.gray)
                            .padding(.top,1)
                            .lineLimit(10)
                            .fixedSize(horizontal: false, vertical: true)
                    }
                }
                .padding(10)
            }
            .background(Color(red: 1, green: 1, blue: 1, opacity: 0.1))
            .cornerRadius(10)
            .padding(.horizontal,5)
            .padding(.bottom,-3)
            .onTapGesture {
                self.isMessageReplyShow = true
            }
            .sheet(isPresented: $isMessageReplyShow,onDismiss: {
                //TODO
            }){
                MailReplyView(user: mailItem.user)
            }
        }
    }
}

这里点击item后会展示MailReplyView回复私信界面:

对应代码如下:

struct MailReplyView: View {
    @Environment(\.presentationMode) var mode
    @State var user: UserItemModel
    @State var content: String = ""
    @State var isLoading: Bool = false
    @State var isModal: Bool = false
    @State var message: String = "回复中"
    
    @State private var alertShow = false
    @State var alertTitle:String = ""
    @State var alertMessage:String = ""
    func startAlert(title:String,message:String){
        self.alertTitle = title
        self.alertMessage = message
        self.alertShow = true
    }
    var body: some View {
        LoadingView(isLoading:self.$isLoading,message: self.$message,isModal:self.$isModal) {
            NavigationView{
                VStack{
                    Form{
                        Section(header: Text("你正在回复 @" + user.userName)) {
                            TextEditor(text: $content)
                                .foregroundColor(.white)
                                .font(.system(size:16))
                                .lineLimit(5)
                        }
                    }
                    Spacer()
                }
                .navigationBarTitle(Text("回复私信"), displayMode: .inline)
                .navigationBarItems(
                    trailing:
                        Button(action: {
                            if self.content.count < 1 {
                                self.startAlert(title: "回复失败", message: "你确定不说点什么吗???")
                                return
                            }
                            self.isLoading = true
                            self.isModal = true
                            HttpRequest(url: "notifications/messages",withAccessToken: true).doPost(postData: ["username":user.userAccount,"content":content]) { (result) in
                                let json = JSON(result)
                                self.isLoading = false
                                self.isModal = false
                                if json["message"].string != nil{
                                    self.startAlert(title: "回复失败", message: json["message"].stringValue)
                                }else{
                                    self.content = ""
                                    self.startAlert(title: "回复成功", message: "你的消息回复成功")
//                                    self.mode.wrappedValue.dismiss()
                                }
                            } errorCallback: {
                                self.isLoading = false
                                self.isModal = false
                                self.startAlert(title: "回复失败", message:"发生了一点小错误,请稍候再试")
                            }
                        }) {
                            Text("回复").foregroundColor(.yellow)
                        })
                
            }
            .preferredColorScheme(.dark)
            .alert(isPresented: $alertShow) {
                Alert(title: Text(alertTitle),message: Text(alertMessage),dismissButton: .default(Text("好的")))
            }
        }
    }
}

注意到这里有句代码:

@Environment(\.presentationMode) var mode

是什么意思呢?

@Environment(.presentationMode) var mode 这句代码用于声明一个环境变量,它使用 @Environment 声明,后面跟着的是环境变量的键值:.presentationMode。
环境变量是 SwiftUI 中的一个特殊类型,它允许我们在不同的视图层次结构中访问一些全局的环境值。在这种情况下,我们声明的环境变量 mode 包含了一个名为 presentationMode 的环境值。这个环境值可以被用于管理当前视图的呈现模式,以便控制其是否显示或被关闭。
例如,如果您在模态视图中提供一个“取消”按钮,您可以在按钮的单击事件中使用 mode.dismiss 来关闭模态视图。

2.3 消息通知

入口如下:

 Section(header: Text("消息中心")) {
    NavigationLink(destination: MailView()) {
        HomeListItem(title: "你的私信",icon:"envelope.circle", color: Color(hex: 0x16c3fc))
    }
    NavigationLink(destination: NotificationView()) {
        HomeListItem(title: "你的通知",icon:"bell.circle", color: Color(hex: 0xf83860))
    }
}

这里点击你的通知后,会跳转到NotificationView。

先声明几个状态变量:

struct NotificationView: View {
    @State var notificationList: [NotificationModel] = []
    @State var waitPlease = false
    @State var isRefreshing = false
    @State var isLoading: Bool = false
    @State var isModal: Bool = false
    @State var message: String = "数据加载中"
    @State var page = 1

然后是视图结构:

var body: some View {
    LoadingView(isLoading:self.$isLoading,message: self.$message,isModal:self.$isModal) {
        ZStack{
            if notificationList.count == 0 && !isLoading {
                VStack{
                    Image(systemName: "doc.text.magnifyingglass")
                        .scaleEffect(3, anchor: .center)
                    Text("暂无查询到的通知").padding(.top,30)
                }
            }
            RefreshView(refreshing: $isRefreshing, action: {
                self.page = 1
                self.getNotificationList(page: self.page)
            }) {
                LazyVStack{
                    ForEach(self.notificationList){ item in
                        NotificationItemView(notificationItem: item)
                            .onAppear(){
                                if !waitPlease && item.id == notificationList[notificationList.count - 1].id {
                                    self.page = self.page + 1
                                    self.getNotificationList(page: self.page)
                                }
                            }
                    }
                }
            }
        }
    }
    .padding(.top,5)
    .navigationBarTitle(Text("消息通知"), displayMode: .inline)
    .onAppear(){
        self.page = 1
        self.getNotificationList(page: self.page)
    }
}

注意到在onAppear的生命周期中调用了接口。

然后对应的消息通知的item视图结构如下:

struct NotificationItemView:View{
    @State var notificationItem: NotificationModel
    @State var placeholderImage = UIImage(named: "Logo")!
    @State var isRepoDetailShow:Bool = false
    @State var isPullRequestShow:Bool = false
    @State var isCommitShow:Bool = false
    @State var isIssueShow:Bool = false
    var body: some View{
        ZStack{
            VStack{
                NavigationLink(destination: RepoDetailView(repoFullPath: self.notificationItem.repo.fullName), isActive: $isRepoDetailShow) { EmptyView() }
                NavigationLink(destination: PullRequestView(repoFullPath: self.notificationItem.repo.fullName), isActive: $isPullRequestShow) { EmptyView() }
                NavigationLink(destination: CommitView(repoFullPath: self.notificationItem.repo.fullName, repoDefaultBranch: "master"), isActive: $isCommitShow) { EmptyView() }
                NavigationLink(destination:  IssuesView(repoPath: self.notificationItem.repo.fullName), isActive: $isIssueShow) { EmptyView() }

            }
            .frame(width: 0, height: 0)
            .opacity(0)
            VStack{
                HStack(alignment: .top) {
                    Image(uiImage: placeholderImage)
                        .resizable()
                        .scaledToFit()
                        .frame(
                            width:40,height:40,
                            alignment: .center
                        )
                        .cornerRadius(5)
                        .onAppear(){
                            guard let url = URL(string: notificationItem.user.userHead) else {
                                return
                            }
                            URLSession.shared.dataTask(with: url) { (data, response, error) in
                                if let data = data, let image = UIImage(data: data) {
                                    placeholderImage = image
                                }
                            }.resume()
                        }
                    VStack(alignment: .leading){
                        HStack(alignment:.top){
                            Text(notificationItem.user.userName)
                                .padding(0)
                            Spacer()
                            Text(notificationItem.time).font(.system(size:12)).foregroundColor(.gray)
                        }
                        Text(notificationItem.message).font(.system(size:14)).foregroundColor(.gray)
                            .padding(.top,1)
                            .lineLimit(10)
                            .fixedSize(horizontal: false, vertical: true)
                    }
                }
                .padding(10)
            }
            .background(Color(red: 1, green: 1, blue: 1, opacity: 0.1))
            .cornerRadius(10)
            .padding(.horizontal,5)
            .padding(.bottom,-3)
            .onTapGesture {
            }
            .contextMenu(ContextMenu {
                Button(action: {
                    self.isRepoDetailShow = true
                }) {
                    HStack{
                        Image(systemName: "archivebox.circle").scaleEffect(1, anchor: .center)
                        Spacer()
                        Text("进入仓库")
                    }
                }
                Divider()
                Button(action: {
                    self.isCommitShow = true
                }) {
                    HStack{
                        Image(systemName: "icloud.and.arrow.up").scaleEffect(1, anchor: .center)
                        Spacer()
                        Text("查看提交")
                    }
                }
                Button(action: {
                    self.isPullRequestShow = true
                }) {
                    HStack{
                        Image(systemName: "shuffle.circle").scaleEffect(1, anchor: .center)
                        Spacer()
                        Text("查看Pull Request")
                    }
                }
                Button(action: {
                    self.isIssueShow = true
                }) {
                    HStack{
                        Image(systemName: "exclamationmark.circle").scaleEffect(1, anchor: .center)
                        Spacer()
                        Text("查看Issue")
                    }
                }
                Divider()
                Button(action: {
                }) {
                    HStack{
                        Image(systemName: "person.circle").scaleEffect(1, anchor: .center)
                        Spacer()
                        Text("用户资料")
                    }
                }
            })
        }
    }
}

这里长按item后会弹出一个menu弹框:

这里怎么弹出来的呢?
答案是通过View的扩展函数contextMenu方法:

contextMenu 方法是 SwiftUI 中的一个视图方法,可以在视图上创建一个上下文菜单。上下文菜单是当用户右键单击视图时显示的菜单。
您可以使用 contextMenu 方法创建一个上下文菜单,并在该菜单中显示选项。

然后点击事件呢?
发现这里只是改变了一个状态变量而已哦。

Button(action: {
    self.isRepoDetailShow = true
}) {
    HStack{
        Image(systemName: "archivebox.circle").scaleEffect(1, anchor: .center)
        Spacer()
        Text("进入仓库")
    }
}

改变了 self.isRepoDetailShow = true
这个变量就跳转了?

原来答案在前面哦:

var body: some View{
    ZStack{
        VStack{
            NavigationLink(destination: RepoDetailView(repoFullPath: self.notificationItem.repo.fullName), isActive: $isRepoDetailShow) { EmptyView() }
            NavigationLink(destination: PullRequestView(repoFullPath: self.notificationItem.repo.fullName), isActive: $isPullRequestShow) { EmptyView() }
            NavigationLink(destination: CommitView(repoFullPath: self.notificationItem.repo.fullName, repoDefaultBranch: "master"), isActive: $isCommitShow) { EmptyView() }
            NavigationLink(destination:  IssuesView(repoPath: self.notificationItem.repo.fullName), isActive: $isIssueShow) { EmptyView() }

        }
        .frame(width: 0, height: 0)

这里相当于监听了 isRepoDetailShow变量,如果监听到了,需要跳转到:
RepoDetailView界面。

其它item类似的。

2.4 加入的组织

入口在这里:

Section(header: Text("你的团队")) {
            NavigationLink(destination:  OrganizationView()) {
                HomeListItem(title: "加入的组织",icon:"person.2.circle", color: Color(hex: 0xb76dda))
            }
//                NavigationLink(destination:  RepoView()) {
//                    HomeListItem(title: "所在的企业",icon:"paperplane.circle", color: Color(hex: 0xffc55a))
//                }
        }

这里会直接跳转到OrganizationView的页面。

先看下状态变量:

struct OrganizationView: View {
    @State var orgList: [OrgModel] = []
    @State var waitPlease = false
    @State var isRefreshing = false
    @State var isLoading: Bool = false
    @State var isModal: Bool = false
    @State var message: String = "数据加载中"
    @State var page = 1

然后是视图结构:

var body: some View {
    LoadingView(isLoading:self.$isLoading,message: self.$message,isModal:self.$isModal) {
        ZStack{
            if orgList.count == 0 && !isLoading {
                VStack{
                    Image(systemName: "doc.text.magnifyingglass")
                        .scaleEffect(3, anchor: .center)
                    Text("暂无查询到的组织").padding(.top,30)
                }
            }
            RefreshView(refreshing: $isRefreshing, action: {
                self.page = 1
                self.getOrgList(page: self.page)
            }) {
                LazyVStack{
                    ForEach(self.orgList){ item in
                        OrgItemView(orgItem: item)
                            .onAppear(){
                                if !waitPlease && item.id == orgList[orgList.count - 1].id {
                                    self.page = self.page + 1
                                    self.getOrgList(page: self.page)
                                }
                            }
                    }
                }
            }
        }
    }
    .padding(.top,5)
    .navigationBarTitle(Text("你的组织"), displayMode: .inline)
    .onAppear(){
        self.page = 1
        self.getOrgList(page: self.page)
    }
}

item定义的视图:

struct OrgItemView:View{
    @State var orgItem: OrgModel
    @State var placeholderImage = UIImage(named: "Logo")!
    var body: some View{
        ZStack{
            VStack{

            }
            .frame(width: 0, height: 0)
            .opacity(0)
            VStack{
                HStack(alignment: .top) {
                    Image(uiImage: placeholderImage)
                        .resizable()
                        .scaledToFit()
                        .frame(
                            width:80,height:80,
                            alignment: .center
                        )
                        .cornerRadius(10)
                        .onAppear(){
                            guard let url = URL(string: orgItem.head) else {
                                return
                            }
                            URLSession.shared.dataTask(with: url) { (data, response, error) in
                                if let data = data, let image = UIImage(data: data) {
                                    placeholderImage = image
                                }
                            }.resume()
                        }
                    VStack(alignment: .leading){
                        HStack(alignment:.top){
                            Text(orgItem.name)
                                .padding(0)
                            Spacer()
                            Text("粉丝:" + String(orgItem.fans)).font(.system(size:12)).foregroundColor(.gray)
                        }
                        Text(orgItem.desc).font(.system(size:14)).foregroundColor(.gray)
                            .padding(.top,1)
                            .lineLimit(10)
                            .fixedSize(horizontal: false, vertical: true)
                    }
                }
                .padding(10)
            }
            .background(Color(red: 1, green: 1, blue: 1, opacity: 0.1))
            .cornerRadius(10)
            .padding(.horizontal,5)
            .padding(.bottom,-3)
            

这个和其他列表页也比较相似。不再过多讲述了。

3 动态

这是首页的第二个Tab页。
首先在这里定义:

struct TabBarView: View {
    @State var selectedBarIndex:Int
    
    var body: some View {
        NavigationView{
            TabView(selection: $selectedBarIndex) {
                HomeView()
                .tabItem {
                    Image(systemName:"briefcase")
                    Text("首页")
                }
                    .tag(0)
                ActivityView()
                .tabItem {
                    Image(systemName:"stopwatch")
                    Text("动态")
                }
                    .tag(1)

这里叫做ActivityView。

先声明动态变量:

struct ActivityView: View {
    @State var activityList: [ActivityModel] = []
    @State var waitPlease = false
    @State var isRefreshing = false
    @State var isLoading: Bool = false
    @State var isModal: Bool = false
    @State var message: String = "数据加载中"
    @State var isLoginShow = false
    @State var lastId = 0 //331672539
    
    @State var myInfoString:JSON? = nil;

然后是视图结构定义:

var body: some View {
    LoadingView(isLoading:self.$isLoading,message: self.$message,isModal:self.$isModal) {
        ZStack{
            if activityList.count == 0 && !isLoading {
                VStack{
                    Image(systemName: "doc.text.magnifyingglass")
                        .scaleEffect(3, anchor: .center)
                    Text("动态读取中").padding(.top,30)
                }
            }
            RefreshView(refreshing: $isRefreshing, action: {
                self.lastId = 0
                self.getActivityList()
            }) {
                LazyVStack{
                    ForEach(self.activityList){ item in
                        ActivityItemView(activity: item)
                        .onAppear(){
                            if !waitPlease && item.id == activityList[activityList.count - 1].id {
                                self.getActivityList()
                            }
                        }
                    }
                }
            }
        }
        .sheet(isPresented: $isLoginShow,onDismiss: {
            self.lastId = 0
            self.getActivityList()
        }){
            LoginView()
                .modifier(DisableModalDismiss(disabled: true))
        }
        .onAppear(){
            self.lastId = 0
            self.getActivityList()
        }
    }
}

具体看下item:

struct ActivityItemView:View{
    @State var activity: ActivityModel
    @State var placeholderImage = UIImage(named: "nohead")!
    @State var isActivePullRequest:Bool = false
    @State var isActiveRepoDetail:Bool = false
    @State var isActiveCommit:Bool = false
    @State var isActiveIssues:Bool = false
    var body: some View{
        NavigationLink(destination: PullRequestView(repoFullPath: activity.repoInfo.fullName), isActive: $isActivePullRequest) { EmptyView() }
        NavigationLink(destination: RepoDetailView(repoFullPath: activity.repoInfo.fullName), isActive: $isActiveRepoDetail) { EmptyView() }
        NavigationLink(destination:
                        CommitView(repoFullPath: activity.repoInfo.fullName, repoDefaultBranch: "master"), isActive: $isActiveCommit) { EmptyView() }
        NavigationLink(destination:  IssuesView(repoPath:activity.repoInfo.fullName), isActive: $isActiveIssues) { EmptyView() }
        
        VStack(alignment: .leading){
            VStack(alignment: .leading){
                HStack(alignment: .top) {
                    Image(uiImage: placeholderImage)
                        .resizable()
                        .scaledToFit()
                        .frame(
                            width:40,height:40,
                            alignment: .center
                        )
                        .cornerRadius(5)
                        .onAppear(){
                            guard let url = URL(string: activity.userInfo.userHead) else {
                                return
                            }
                            URLSession.shared.dataTask(with: url) { (data, response, error) in
                                if let data = data, let image = UIImage(data: data) {
                                    placeholderImage = image
                                }
                            }.resume()
                        }
                    VStack(alignment: .leading){
                        HStack(alignment:.top){
                            Text(activity.userInfo.userName)
                                .padding(0)
                            Spacer()
                            Text(activity.createTime).font(.system(size:12)).foregroundColor(.gray)
                        }
                        Text(getActivityContent(activity:activity)).font(.system(size:14)).foregroundColor(.gray)
                            .padding(.top,1)
                            .lineLimit(10)
                            .fixedSize(horizontal: false, vertical: true)
                    }
                }
            }
            .padding(10)
            Spacer()
        }
//        .frame(height:100)
        .background(Color(red: 1, green: 1, blue: 1, opacity: 0.1))
        .cornerRadius(10)
        .padding(.horizontal,5)
        .padding(.top,5)

4 总结

  • 需要理解下@State注解用法,这个主要是一个状态变量,可以理解成 didSet,就是有一个回调作用,在Swift UI中会通知更新界面元素。 或者可以理解成Android中的LiveData变量。

  • View里面扩展了一个方法navigationBarItems,是标题栏的右侧点击事件,然后有个sheet方法,是类似模态框,里面的闭包View就是要弹出的视图。

  • Appear声明周期,就是首次可见的生命周期,一般可以在这里面请求网络。同时最好配合LoadingView进行网络加载,一般是用一个加载圈包裹列表视图,通过isLoading变量控制加载器的显示或隐藏。

  • 这里有个Environment的注解,主要是定义了全局的环境值,我们可以用来管理当前视图的显示或关闭。

  • View里面扩展了一个contextMenu方法,可以用来显示上下文菜单。然后通过在顶部配置NavigationLink来监听isActive定义的变量,当点击某个上下文菜单,我们改变这个状态变量,然后通知到这里isActive,然后就可以跳转到destination定义的View页面了。


   转载规则


《iOS swift Gitee客户端 完整项目分析之二》 Jason 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
iOS swift Gitee客户端 完整项目分析之三 iOS swift Gitee客户端 完整项目分析之三
1 项目地址 https://gitee.com/open-gitee/gitee_iosGitee基于SwiftUI和OpenApi的iOS客户端项目。 2 设置页面首先看下设置tab页效果: 2.1 入口SettingView()
2023-02-12
下一篇 
Android wear os 使用入门 Android wear os 使用入门
1 Wear OS 开发原则1.1 针对关键任务进行设计重点关注目标用户的一项或两项需求,而不是完整应用体验。不要迁移整个移动代码库,也不要将 Wear OS 界面放在顶层。 相反,您应寻找适合腕部佩戴的关键任务,并简化 Wear OS 的
2023-02-11
  目录