Swift版百思不得姐

文藝範兒的小貓咪發表於2018-04-03

經同學建議,發覺寫的確實有些亂,趁著上班前的時間好好對模組整理一下##

最近趁著專案需求不是很緊,利用功能完成的空餘時間將之前寫的一個百思不得姐專案改寫成了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版百思不得姐連結地址

相關文章