Swift版百思不得姐

作者: 皮乐皮儿 | 来源:发表于2016-11-10 20:12 被阅读304次

    经同学建议,发觉写的确实有些乱,趁着上班前的时间好好对模块整理一下##

    最近趁着项目需求不是很紧,利用功能完成的空余时间将之前写的一个百思不得姐项目改写成了swift版,不得不说,swift版让我抓狂,严谨的语法结构动不动就奔溃,简直要了老命了。 学习swift时间不长,大都是零零碎碎的看了些基础知识点,发觉看文档索然无味,就自己试着将现有的项目改写一番,过程是痛苦的,收获确实大大的。

    在这里我主要通过精华,发布,关注,登录,我的以及推荐关注界面进行几个模块的简单实现介绍,由于敲代码也一年了,语言能力已经退化,表述不好的直接上代码,嘿嘿~~~

    一 项目的整体框架搭建

    写过项目的都知道,最常用的框架就是TabBarController + NavigationController的形式,这里采用的依旧是经典式样,具体是实现和OC是一样的,同样包括自定义tabbar,下面上部分代码展示一下:

    代码块:

        private func setupChildVcs() {
        
            setupChildVc(vc: EssenseController(),title:"精华",image:"tabBar_essence_icon",selectedImage:"tabBar_essence_click_icon")
            setupChildVc(vc: NewViewController(),title:"新帖",image:"tabBar_new_icon",selectedImage:"tabBar_new_click_icon")
            setupChildVc(vc: FriendThrendController(),title:"关注",image:"tabBar_friendTrends_icon",selectedImage:"tabBar_friendTrends_click_icon")
            setupChildVc(vc: ProfileController(),title:"我",image:"tabBar_me_icon",selectedImage:"tabBar_me_click_icon")
    
        }
        
        
        private func setupTabBar() {
            setValue(TabBar(), forKeyPath: "tabBar");
            
        }
        
        private func setupChildVc(vc:UIViewController,title:String,image:String,selectedImage:String) {
        
            let nav = NavigationController(rootViewController: vc)
            
            addChildViewController(nav)
            
            nav.tabBarItem.title = title
            
            nav.tabBarItem.image = UIImage(named: image)
            
            nav.tabBarItem.selectedImage = UIImage(named: selectedImage)
            
            
        }
    

    主要是对整体框架的搭建,有一点我觉得还挺好的,就是在自定义tabbar中添加加号按钮

    override func layoutSubviews() {
            super.layoutSubviews()
            
            publishButton?.center = CGPoint(x: width * 0.5, y: height * 0.5)
            let buttonY:CGFloat = 0
            let buttonW = width / 5
            let buttonH = height
            var index:Int = 0
            var buttonX:CGFloat = 0
    
            for  button in subviews {
                if !button.isKind(of: NSClassFromString("UITabBarButton")!){
                    continue
                }            
                buttonX = buttonW * CGFloat((index > 1 ? index + 1 : index))
                button.frame = CGRect(x: buttonX, y: buttonY, width: buttonW, height: buttonH)
                index += 1
            }
            
            publishButton?.addTarget(self, action: #selector(TabBar.publishButtonClick), for: .touchUpInside)
            
        }
    

    不过在写的过程中是比较苦逼的,swift中不同类型的常量是不能进行四则运算的,必须要转化为同一类型才可以,这一点在写的过程中简直痛不欲生,不过好在Xcode直接就报错提醒,这样就可以及时改正,说到这一点,我想说的是,写完整个小项目下来,我几乎是边写边查边学,swift语法的严谨让人抓狂也让人欣喜

    整体框架介绍完毕,下面来开始分模块进行介绍吧:

    二 精华/新帖模块

    1.网络请求及数据解析部分

    这个模块主要是展现列表,列表形式包括视频,声音,图片和段子的混合,这样就需要定制cell了,这里实现形式采用的是tableView,不过没有复用tableView,这一点上如果想节约内存,可以采用复用两个或者三个tableView的方式,这里就不多做介绍,网上有相关的学习资料,不明白的可以去学习一下。

    因为网络请求大都是一样的,所以我用了一个共同类TopViewController来进行列表布局

    extension TopViewController {
    
        override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
          return viewModel.topicArray.count
        }
        
        override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            let cell  = tableView.dequeueReusableCell(withIdentifier: "topic", for: indexPath) as! TopicCell
            
            cell.topicModel = viewModel.topicArray[indexPath.row] as! TopicModel
            
            return cell
        }
        
        override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            
            let topicModel = viewModel.topicArray[indexPath.row] as! TopicModel
            
            return topicModel.cellHeight
        }
    }
    

    网络请求我是放到了一个叫TopicViewModel的类中,控制器只需要将page参数和tableView传递过去,就不用管别的了

    func loadTopicModel() {
            viewModel.topicArray.removeAllObjects()
            viewModel.page = 0
            viewModel.loadTopicDataFromNet()
        }
        
        func loadMoreTopicModel() {
            viewModel.page += 1
            tableView.mj_footer.beginRefreshing()
            viewModel.loadTopicDataFromNet()
        }
    

    这种技巧和我在网上搜的一些大神给的MVVM demo有些像,不过我觉得这并不是真正的MVVM,只是对控制器做了一定的瘦身,本质上应该还是属于MVC的,关于这点在推荐关注模块中我会大致说一下瘦身的具体方法,这里就直接上代码片段吧:

    func loadTopicDataFromNet() {
         
            var parameters = [String:Any]()
            
            parameters["a"] = a
            
            parameters["c"] = "data"
            
            parameters["type"] = type
            
            parameters["page"] = page
            
            if maxtime != nil && (maxtime?.characters.count)! > 0  {
                parameters["maxtime"] = maxtime
            }
            self.parameters = parameters as NSDictionary?
            
            NetWorkTool.NetWorkToolGet(urlString: "http://api.budejie.com/api/api_open.php", parameters: parameters, success: { (responseObj) in
                
                if self.parameters != (parameters as NSDictionary?) {
                    return
                }
                self.maxtime = (responseObj?["info"] as! [String:Any])["maxtime"] as! String?
                
                var array = [Any]()
                for topicModel in TopicModel.mj_objectArray(withKeyValuesArray: responseObj!["list"]) {
                array.append(topicModel)
                }
                
                self.topicArray.addObjects(from: array)
                self.tableView.reloadData()
                self.tableView.mj_header.endRefreshing()
                self.tableView.mj_footer.endRefreshing()
                
                }) { (error) in
                    if self.parameters != (parameters as NSDictionary?) {
                        return
                    }
                    self.tableView.mj_header.endRefreshing()
                    self.tableView.mj_footer.endRefreshing()
                    if self.page > 0 {
                    
                        self.page -= 1
                    }
            }
            
        }
    

    网络请求采用的是AFN,刚开始做的时候想用Alamofire这个框架来写,但是用起来不是很顺手,所以还是用了常用的AFN,数据解析用的是MJExtension,方法和OC类似的,只不过swift对数据类型的要求比较严格,在用的时候需要多加注意,按照提示错误进行对应修改即可,特别是关于可选类型和闭包,建议在写之前能透彻理解,当然也可以边写边理解,我就属于后者

    2. cell的定制

    cell的定制,主要采用的是xib和代码结合的方式,xib用起来真的挺方便的,避免了大量代码的书写,具体的实现可以下载代码看一下,这里只上几张图进行展示,另外一点要说的是,cell的高度,我是放到model里面进行计算处理的,这一点上,在初次想以这种方法实现的时候不知道从何下手,后来问过写过的童鞋,他告诉了我,具体是这样的,主要在于didSet,因为Swift不像OC,没有setter方法

    var text:String? {
            
            didSet {
                if cellHeight == 0.0 {
                    cellHeight = ConstTool.instance.TopicTextY
                    let maxSize = CGSize(width: ConstTool.instance.kScreenW - 4 * ConstTool.instance.TopicMargin, height: CGFloat(MAXFLOAT))
                    let textStr = NSString(string: text!)
                    let textH = textStr.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName:UIFont.systemFont(ofSize: 14)], context: nil).size.height
                    cellHeight += textH + ConstTool.instance.TopicMargin
                    
                    let contentW = maxSize.width
                    var contentH = contentW * height / width
                    let contentX = ConstTool.instance.TopicMargin
                    let contentY = cellHeight
                    if type == 10 {
                        if contentH > ConstTool.instance.TopicPictureMaxH {
                            contentH = ConstTool.instance.TopicPictureDefaultH
                            hiddenSeebigButton = true
                        }else {
                            hiddenSeebigButton = false
                        }
                        pictureF = CGRect(x: contentX, y: contentY, width: contentW, height: contentH)
                        cellHeight += contentH + ConstTool.instance.TopicMargin
                    }else if (type == 31) {
                        
                        voiceF = CGRect(x: contentX, y: contentY, width: contentW, height: contentH)
                        cellHeight += contentH + ConstTool.instance.TopicMargin
                    }else if(type == 41) {
                        
                        videoF = CGRect(x: contentX, y: contentY, width: contentW, height: contentH)
                        cellHeight += contentH + ConstTool.instance.TopicMargin
                    }
                    
                    if top_cmt != nil {
                        let commentContent = NSString(format: "%@:%@", (top_cmt?.user?.username)!,(top_cmt?.content)!)
                        
                        let commentContentH = commentContent.boundingRect(with: maxSize, options: .usesLineFragmentOrigin, attributes: [NSFontAttributeName:UIFont.systemFont(ofSize: 13)], context: nil).size.height
                        cellHeight += commentContentH + ConstTool.instance.TopicMargin
                    }
                    
                    cellHeight += ConstTool.instance.TopicBottomH + ConstTool.instance.TopicMargin
                }   
            }   
        }
    

    这个可以按照OC的Setter方法来理解

    下面是几张截图:

    Simulator Screen Shot 2016年11月10日 下午6.22.20.png Simulator Screen Shot 2016年11月10日 下午6.22.24.png Simulator Screen Shot 2016年11月10日 下午6.22.30.png Simulator Screen Shot 2016年11月10日 下午6.22.33.png

    三 发布界面

    点击加号按钮之后是一个动画展示的发布界面,这个效果看起来挺酷炫的,其实实现起来也不是很难,具体用到的是一个叫pop的第三方库

    Simulator Screen Shot 2016年11月10日 下午8.00.52.png

    主要的代码如下:

        func setupButtons() {
            let maxCols = 3
            let buttonW = 72
            let buttonH = buttonW + 30
            let buttonStartX = 15
            let buttonMargx = (ConstTool.instance.kScreenW - CGFloat(maxCols * buttonW) - 2 * CGFloat(buttonStartX)) / CGFloat(maxCols - 1)
            let buttonStartY = (ConstTool.instance.kScreenH - 2 * CGFloat(buttonH)) / CGFloat(2)
            for  i in 0 ..< imagesArr.count {
                let button = VerticalButton(type: .custom)
                let row = i / maxCols
                let col = i % maxCols
                let buttonX = CGFloat(buttonStartX) + (buttonMargx + CGFloat(buttonW)) * CGFloat(col)
                
                let buttonEndY = buttonStartY + CGFloat(row) * CGFloat(buttonH)
                button.setImage(UIImage(named:imagesArr[i]), for: .normal)
                button.setTitle(titlesArr[i], for: .normal)
                button.setTitleColor(UIColor.black, for: .normal)
                button.titleLabel?.font = UIFont.systemFont(ofSize: 12)
                addSubview(button)
                button.addTarget(self, action: #selector(PublishView.clickButton(button:)), for: .touchUpInside)
                
                button.tag = i
                let animation = POPSpringAnimation(propertyNamed: kPOPViewFrame)
                animation?.fromValue = NSValue.init(cgRect: CGRect(x: buttonX, y: CGFloat(buttonH) - ConstTool.instance.kScreenH, width: CGFloat(buttonW), height: CGFloat(buttonH)))
                animation?.toValue = NSValue.init(cgRect: CGRect(x: buttonX, y: CGFloat(buttonEndY), width: CGFloat(buttonW), height: CGFloat(buttonH)))
                animation?.springBounciness = 5
                animation?.springSpeed = 15
                animation?.beginTime = CACurrentMediaTime() + CFTimeInterval(0.1 * CGFloat(i))
                button.pop_add(animation, forKey: nil)
            }
            
            sloganView = UIImageView(image: UIImage(named: "app_slogan"))
            
            addSubview(sloganView)
            
            let centerX:CGFloat = ConstTool.instance.kScreenW * 0.5
            let centerEndY:CGFloat = ConstTool.instance.kScreenH * 0.2
            let centerBeginY:CGFloat = centerEndY - ConstTool.instance.kScreenH
            let animation = POPSpringAnimation(propertyNamed: kPOPViewCenter)
            animation?.fromValue = NSValue.init(cgPoint: CGPoint(x: centerX, y: centerBeginY))
            animation?.toValue = NSValue.init(cgPoint: CGPoint(x: centerX, y: centerEndY))
            animation?.springBounciness = 5
            animation?.springSpeed = 15
            animation?.beginTime = CACurrentMediaTime() + CFTimeInterval(0.1 * CGFloat(imagesArr.count))
            animation?.completionBlock = { (animation,finished) in
            
            self.isUserInteractionEnabled = true
            }
            sloganView.pop_add(animation, forKey: nil)
        }
    

    有兴趣的同僚可以下载源代码去好好研究一下pop,这个在github上都可以搜索到

    四 登录模块

    这个模块没有实现登录功能,只是布局了登录和注册的界面,主要用的是xib布局

    Simulator Screen Shot 2016年11月10日 下午8.01.17.png

    点击注册账号是动画出现注册界面,主要靠的是改变登录界面的左侧的约束

        @IBAction func shouRegisterView(_ sender: UIButton) {
            if leftConstraint.constant == 0 {
                leftConstraint.constant = -view.width
                sender.isSelected = true
            }else {
                leftConstraint.constant = 0
                sender.isSelected = false
            }
           
            UIView.animate(withDuration: 0.3, animations: animation)
        }
        
        func animation() {
            self.view.layoutIfNeeded()
        }
    

    五 推荐关注模块

    这个模块我想介绍一下将tableView抽出来,极大可能给控制器瘦身的实现

    override func viewDidLoad() {
            super.viewDidLoad()
    
            // Do any additional setup after loading the view.
            
            navigationItem.title = "推荐关注"
            view.backgroundColor = ConstTool.instance.GlobalColor()
            automaticallyAdjustsScrollViewInsets = false
            
            categoryView.contentInset = UIEdgeInsets(top: 64, left: 0, bottom: 0, right: 0)
            categoryListView.contentInset = UIEdgeInsets(top: 64, left: 0, bottom: 0, right: 0)
         
            categoryView.delegate = self.manager
            categoryListView.delegate = self.manager
            
            categoryListView.dataSource = self.manager
            categoryView.dataSource = self.manager
            
            self.viewModel.categoryView = categoryView
            self.viewModel.categoryListView = categoryListView
            
            self.manager.viewModel = self.viewModel
            
            
            self.viewModel.getRecommendCategoryDataFromNet()
            
        }
    

    推荐关注控制器中就只有这几行代码,是不是感觉很清爽呢?我也觉得很清爽,这里的tableView的代理方法和数据源方法,我是抽出来放到一个继承于NSObject的manager类中,将tableView的delegate和datasource设置成manager就可以了

    在manager中,要关联一个管理数据的类,称之为viewModel,以后的数据源和UI交互就靠这两个类,控制器几乎可以完全隔离出来了,这样就降低了耦合度,修改起来也很方便

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        
            if tableView.tag == 1 {
                
                return viewModel.categoryArray.count
            }
            tableView.mj_footer.isHidden = (viewModel.categoryListArray.count == 0)
            return viewModel.categoryListArray.count
        }
        
        func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
            if tableView.tag == 1 {
                let cell = tableView.dequeueReusableCell(withIdentifier: "categoryCell", for: indexPath) as! CategoryCell
                cell.categoryModel = viewModel.categoryArray[indexPath.row] as! CategoryModel
                
                return cell
            }
            
            let cell  = tableView.dequeueReusableCell(withIdentifier: "categoryListCell", for: indexPath) as! CategoryListCell
            cell.categoryListModel = viewModel.categoryListArray[indexPath.row] as! CategoryListModel
            return cell
            
        }
        
        func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
            
            if tableView.tag == 2 {
                return 80
            }
            return 44
        }
        
        func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
            
            if tableView.tag == 1 {
                viewModel.categoryListView.mj_footer.endRefreshing()
                let model = viewModel.categoryArray[indexPath.row] as! CategoryModel
                viewModel.page = 1
                viewModel.getRecommendCategoryListDataFromNetWithCategoryId(ID: model.ID)
                
            }
        }
    

    manager主要管理的就是这几个方法,其中的viewModel是另外一个类,如下:

        func getRecommendCategoryDataFromNet() {
            
            SVProgressHUD.show()
            
            var parameters = [String:Any]()
            
            parameters["a"] = "category"
            parameters["c"] = "subscribe"
            let manager = AFHTTPSessionManager()
            
             manager.get("http://api.budejie.com/api/api_open.php", parameters: parameters, progress: {(progress) in
            
            }, success: { (task, responseObj)  in
            SVProgressHUD.dismiss()
                if responseObj != nil {
                    
                    let response = responseObj as! [String:Any]
                    
                    self.categoryArray = CategoryModel.mj_objectArray(withKeyValuesArray: response["list"])
                    
                    self.categoryView.reloadData()
                    
                    let indexPath = NSIndexPath(row: 0, section: 0)
                    
                    self.categoryView.selectRow(at: indexPath as IndexPath, animated: false, scrollPosition: .top)
                    let model = self.categoryArray[0] as! CategoryModel
                    
                    self.getRecommendCategoryListDataFromNetWithCategoryId(ID: model.ID)
                    
                }
                
            }, failure: {(task, error )  in
            
                SVProgressHUD.showError(withStatus: "分类数据加载失败")
            })
            
        }
    

    数据回来,只需要tableView.reloadData()一下就可以改变布局了

    Simulator Screen Shot 2016年11月10日 下午8.01.27.png

    具体的实现就是这样,但是如果处理起来比较复杂的逻辑,这种可能就不是那么简单了,个人觉得比较适合tableView展示数据的类型,这种方式倒可以多多尝试

    六 我的界面模块

    这个模块比较简单,这里就不多做描述,具体见demo,下面附一张图:

    Simulator Screen Shot 2016年11月10日 下午8.01.20.png

    就写这么多吧,具体的demo我已经上传到gitHub上,有兴趣的同僚可以前往下载瞅瞅,因为是初次用swift写代码,很多地方处理的还不到位,功能实现的也比较单一简单,后续如果有机会,会将功能再完善处理:

    swift版百思不得姐链接地址

    相关文章

      网友评论

        本文标题:Swift版百思不得姐

        本文链接:https://www.haomeiwen.com/subject/azpxpttx.html