從頭開始寫一款開源app上線,相互學(xué)習(xí)(二)

在上一篇文章從頭開始寫一款開源app上線,相互學(xué)習(xí)(一),我們已經(jīng)將項(xiàng)目的框架及第三方庫,還有網(wǎng)絡(luò)層都搭建好了,接下來的工作就是界面搭建
先看看新聞列表效果圖:

新聞列表效果圖

界面搭建

新聞頁分析

1.左右滾動的分類條
2.上下滾動的列表
3.左右滾動切換不同分類的列表
4.分類條與列表之間的聯(lián)動效果


界面分析

控件布局分析

1.分類條使用一個View分離
2.列表區(qū)支持左右滾動,使用一個collectionView于分類條下面,collectionView的cell對應(yīng)一個列表
3.為了使每個列表加載完并緩存起來,使用containerView將每個列表都用一個控制器去加載,并將其緩存
4.列表使用tableView進(jìn)行展示


控件布局分析圖

代碼分析

先看看聚合返回的數(shù)據(jù)結(jié)構(gòu)


聚合的數(shù)據(jù)結(jié)構(gòu)

主控制器中,將分類條(topicView)及列表區(qū)(collectionView)作為屬性

 /// 標(biāo)題滾動條
 ///swift的懶加載有寫法1(直接初始化):
    lazy var topicView: OYNewsTopicView = OYNewsTopicView()
    let topicArr = NewsTopicKeys
    
    /// 新聞列表用collection包裹
    let flowLayout: UICollectionViewFlowLayout = UICollectionViewFlowLayout()
 ///swift的懶加載有寫法2(將懶加載代碼寫在閉包中,閉包要加上(),表示執(zhí)行閉包):
    lazy var collectionView: UICollectionView = {
        return UICollectionView(frame: CGRect.zero, collectionViewLayout: self.flowLayout)
    }()

/// 控制器緩存,用于將每個列表的控制器進(jìn)行緩存 
    var channelVcCache: [String : OYNewsChannelVC] = [String : OYNewsChannelVC]()

1.分類條,聚合提供的類型是固定不變的,將類型做為常量作為分類條的數(shù)據(jù)源

let NewsTopicKeys: [String] = ["top", "yule", "shehui", "keji", "shishang", "tiyu",  "guonei", "guoji", "junshi", "caijing"]
let NewsTopics: [String] = ["頭條", "娛樂", "社會", "科技", "時尚", "體育", "國內(nèi)", "國際", "軍事", "財(cái)經(jīng)"]

2.列表區(qū),是一個collectionView, 其每一個cell對應(yīng)一個列表, 做法如下:

返回cell的數(shù)據(jù)源方法
 func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "OYChannelCell", for: indexPath)
        for view in cell.subviews {
            guard view != cell.contentView else {
                continue
            }
            view.removeFromSuperview()
        }
        // 根據(jù)topic進(jìn)行對每個列表的控制器進(jìn)行緩存 
        let vc = channelVc(withTopic: topicArr[indexPath.item])
        cell.contentView.addSubview(vc.view)
        vc.view.frame = cell.bounds

        return cell
    }

// 根據(jù)topic從緩存中返回對應(yīng)的控制器,如果沒有,則創(chuàng)建新的控制器并緩存 
// 方法前加上private表示為私有函數(shù)
 private func channelVc(withTopic topic: String) -> OYNewsChannelVC {
        guard let vc = channelVcCache[topic] else {
            let newVc = OYNewsChannelVC()
            // 強(qiáng)引用控制器并緩存,必須強(qiáng)引用控制器,因?yàn)橹皇菍⒖刂破鱲iew添加到cell上, 當(dāng)cell移出屏幕,view就會被釋放掉
            addChildViewController(newVc)
            channelVcCache[topic] = newVc
            newVc.type = topic
            return newVc
        }
        return vc
    }

3.列表cell,使用自動布局,自動計(jì)算行高,聚合上提供三個字段為圖片,所以設(shè)計(jì)cell時,支持1-3張圖片的自適應(yīng). 后來發(fā)現(xiàn),聚合返回的三張圖片都是一樣的,只是尺寸不同, 并且第二張和第三張是一樣的url,即同一張圖片,所以現(xiàn)在app上顯示出來的都是兩張圖片的樣式

/// 自動布局要點(diǎn): 將控件從上往下進(jìn)行約束,tableView要設(shè)置兩個屬性:
        tableView.estimatedRowHeight = 100 // 預(yù)估行高,作用是讓數(shù)據(jù)源方法先返回cell,再去計(jì)算行高
        tableView.rowHeight = UITableViewAutomaticDimension // 自動計(jì)算行高
這樣,cell就會根據(jù)從上到下的約束自動計(jì)算出高度,所以約束一定要寫正確

/// 自動布局代碼
func setupUI() -> Void {
        contentView.addSubview(titleLabel)
        contentView.addSubview(picView1)
        contentView.addSubview(picView2)
        contentView.addSubview(picView3)
        contentView.addSubview(authorLabel)
        contentView.addSubview(dateLabel)
        let width = (mainWidth-2*pictureInterMargin-2*leftRightMargin)/3
        titleLabel.numberOfLines = 0
        titleLabel.snp.makeConstraints { (make) in
            make.top.equalTo(contentView).offset(topBottomMargin)
            make.left.equalTo(contentView).offset(leftRightMargin)
            make.right.equalTo(contentView).offset(-leftRightMargin)
        }
        picView1.snp.makeConstraints { (make) in
            make.top.equalTo(titleLabel.snp.bottom).offset(interMargin)
            make.left.equalTo(contentView).offset(leftRightMargin)
            make.width.equalTo(width)
            // 因?yàn)椴恢缊D片的尺寸,所以我都按寬高比4/3進(jìn)行設(shè)置
            make.height.equalTo(width).multipliedBy(0.75)
        }
        picView2.snp.makeConstraints { (make) in
            make.top.equalTo(picView1)
            make.left.equalTo(picView1.snp.right).offset(pictureInterMargin)
            make.width.height.equalTo(picView1)
        }
        picView3.snp.makeConstraints { (make) in
            make.top.equalTo(picView2)
            make.left.equalTo(picView2.snp.right).offset(pictureInterMargin)
            make.width.height.equalTo(picView2)
        }
        authorLabel.textColor = #colorLiteral(red: 0.7233663201, green: 0.7233663201, blue: 0.7233663201, alpha: 1)
        authorLabel.font = UIFont.systemFont(ofSize: 13)
        authorLabel.snp.makeConstraints { (make) in
            make.left.equalTo(contentView).offset(leftRightMargin)
            make.top.equalTo(picView1.snp.bottom).offset(interMargin)
        }
        dateLabel.textColor = #colorLiteral(red: 0.7233663201, green: 0.7233663201, blue: 0.7233663201, alpha: 1)
        dateLabel.font = UIFont.systemFont(ofSize: 13)
        dateLabel.snp.makeConstraints { (make) in
            make.left.equalTo(authorLabel.snp.right).offset(leftRightMargin)
            make.centerY.equalTo(authorLabel)
            make.bottom.equalTo(contentView).offset(-topBottomMargin)
        }
        contentView.snp.makeConstraints { (make) in
            make.edges.equalTo(self)
        }
    }
模型的didSet方法, 相當(dāng)于OC中重寫set方法
var model: OYNewsModel? {
        didSet {
            let newModel = model!
            titleLabel.text = newModel.title
            authorLabel.text = newModel.author_name
            /// 對時間戳顯示的處理, 我寫了個分類, 有興趣可以代碼中查看
            dateLabel.text = NSDate.dateString(dateString: newModel.date, dateFormat: "yyyy-MM-dd HH:mm")
            /// 針對iOS10做的處理:sizeToFit()
            titleLabel.sizeToFit()
            authorLabel.sizeToFit()
            dateLabel.sizeToFit()
            let width = (mainWidth-CGFloat(newModel.picArr.count-1)*pictureInterMargin-2*leftRightMargin)/CGFloat(newModel.picArr.count)
            // 更新約束
            picView1.snp.updateConstraints { (make) in
                make.width.equalTo(width)
                make.height.equalTo(width*0.75)
            }
            let placeImage = UIImage(named: "colorBg")!
            /// 根據(jù)圖片數(shù)量顯示imageView
            switch newModel.picArr.count {
            case 1:
                picView2.isHidden = true
                picView3.isHidden = true
                picView1.sd_setImage(with: URL(string: newModel.picArr[0]), placeholderImage: placeImage)
                break
            case 2:
                picView2.isHidden = false
                picView3.isHidden = true
                picView1.sd_setImage(with: URL(string: newModel.picArr[0]), placeholderImage: placeImage)
                picView2.sd_setImage(with: URL(string: newModel.picArr[1]), placeholderImage: placeImage)
                break
            case 3:
                picView2.isHidden = false
                picView3.isHidden = false
                picView1.sd_setImage(with: URL(string: newModel.picArr[0]), placeholderImage: placeImage)
                picView2.sd_setImage(with: URL(string: newModel.picArr[1]), placeholderImage: placeImage)
                picView3.sd_setImage(with: URL(string: newModel.picArr[2]), placeholderImage: placeImage)
                break
            default:
                break
            }
            /// iOS10,layoutSubviews的方法調(diào)用次數(shù)減少,手動調(diào)用一次
            layoutIfNeeded()
        }
    }

對于iOS10,使用masonry或snapKit進(jìn)行自動布局有不同的地方,原因在于ios10對于layoutSubviews的方法調(diào)用次數(shù)減少了,具體可看我之前寫的文章:iOS10后使用Masonry進(jìn)行自動布局出現(xiàn)的問題及處理

4.分類條與列表區(qū)的聯(lián)動效果
這部分會比較繁瑣,我們應(yīng)該先把問題列出來:

  • 點(diǎn)擊分類條上某個分類, 列表區(qū)滾動到相應(yīng)的列表
  • 列表區(qū)左右滾動,分類條實(shí)時跟著滾動的偏移量對相應(yīng)的兩個分類顏色進(jìn)行調(diào)節(jié)
  • 列表區(qū)滾動停止時,以及點(diǎn)擊某個分類時, 分類條自動將當(dāng)前選中的分類滾動到最中間, 如果當(dāng)前的分類處于屏幕寬度的前半段或后半段時,則分類條只需滾動到前半段或后半段
在分類條中,我寫了兩個屬性
/// 當(dāng)前滾動的進(jìn)度,用于改變topic的顏色
var process: CGFloat = 0 {
        didSet {  // didSet相當(dāng)于OC中重寫set方法
            let intProcess = Int(process)
            let firstProcess = process - CGFloat(intProcess)
            let secondProcess = CGFloat(intProcess+1) - process
            
            let firstModel = self.dataSource[intProcess]
            firstModel.process = 1-firstProcess
            // 實(shí)時刷新分類條的collectionView,改變顏色
            if intProcess == self.dataSource.count-1 {// 防止數(shù)組越界
                collectionView.reloadItems(at: [IndexPath(item: intProcess, section: 0)])
                return
            }
            let secondModel = self.dataSource[intProcess+1]
            secondModel.process = 1-secondProcess
            collectionView.reloadItems(at: [IndexPath(item: intProcess, section: 0), IndexPath(item: intProcess+1, section: 0)])
        }
    }
/// 當(dāng)前滾動的進(jìn)度,用于改變topic的偏移量
var processInt: Int = 0 {
        didSet {
            /// 記錄indexPath
            lastIndexPath = IndexPath(item: processInt, section: 0)
            let offsetX = (CGFloat(processInt)+0.5)*flowLayout.itemSize.width
            if offsetX < mainWidth/2 { // 滾動到前半段
                collectionView.setContentOffset(CGPoint.zero, animated: true)
                return
            }
            if (collectionView.contentSize.width-offsetX) < mainWidth/2 { // 滾動到后半段
                collectionView.setContentOffset(CGPoint(x: collectionView.contentSize.width-mainWidth, y: 0), animated: true)
                return
            }
           // 將選中的分類滾動到最中間
            collectionView.setContentOffset(CGPoint(x: offsetX-mainWidth/2, y: 0), animated: true)
        }
    }

解決剛才提出的三個問題:
在點(diǎn)擊某個分類時,將上一個分類的process置為0,并回調(diào)給主控制器,主控制器會將列表區(qū)滾動到相應(yīng)的列表

分類條的collectionView代理方法
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        // 刷新上一個cell
        let model = self.dataSource[lastIndexPath.item]
        model.process = 0
        collectionView.reloadItems(at: [lastIndexPath])
        // 回調(diào)給主控制器
        didSelectTopic?(indexPath.item)
        lastIndexPath = indexPath
        // 分類條也滾動到相應(yīng)的位置上
        processInt = indexPath.item
    }

在列表區(qū)滾動時,實(shí)時將當(dāng)前滾動的process傳遞給分類條,實(shí)時更新顏色
在列表區(qū)滾動停止時,將當(dāng)前滾動哪一個分類傳遞給分類條,更新分類條的偏移量

列表區(qū)的collecttionView的代理方法
func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let offsetX = scrollView.contentOffset.x
        /// 當(dāng)列表區(qū)X方向的偏移量<0或超過其長度時不回調(diào)
        guard offsetX >= 0 && offsetX <= scrollView.contentSize.width-mainWidth else {
            return
        }
        let process = offsetX / mainWidth
        topicView.process = process
    }
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        let processInt = Int(scrollView.contentOffset.x / mainWidth)
        topicView.processInt = processInt
    }

好了,新聞列表的知識點(diǎn)已經(jīng)都寫出來了,具體代碼請到gitHub上下載:TopOmnibus
下一篇介紹: 微信精選列表的做法, 及點(diǎn)擊后的跳轉(zhuǎn)處理從頭開始寫一款開源app上線,相互學(xué)習(xí)(三)
如有什么不清楚,請留言,或者直接聯(lián)系我; 有錯誤的,請指出,謝謝

項(xiàng)目已上線,AppStore下載:新聞巴士

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末惭笑,一起剝皮案震驚了整個濱河市土浸,隨后出現(xiàn)的幾起案子这溅,更是在濱河造成了極大的恐慌员舵,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捉兴,死亡現(xiàn)場離奇詭異蝎困,居然都是意外死亡录语,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門禾乘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來澎埠,“玉大人,你說我怎么就攤上這事始藕∑盐龋” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵伍派,是天一觀的道長江耀。 經(jīng)常有香客問我,道長诉植,這世上最難降的妖魔是什么祥国? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮倍踪,結(jié)果婚禮上系宫,老公的妹妹穿的比我還像新娘。我一直安慰自己建车,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布椒惨。 她就那樣靜靜地躺著缤至,像睡著了一般。 火紅的嫁衣襯著肌膚如雪康谆。 梳的紋絲不亂的頭發(fā)上领斥,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機(jī)與錄音沃暗,去河邊找鬼月洛。 笑死,一個胖子當(dāng)著我的面吹牛孽锥,可吹牛的內(nèi)容都是我干的嚼黔。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼惜辑,長吁一口氣:“原來是場噩夢啊……” “哼唬涧!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起盛撑,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤碎节,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后抵卫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體狮荔,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡胎撇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了殖氏。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片创坞。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖受葛,靈堂內(nèi)的尸體忽然破棺而出题涨,到底是詐尸還是另有隱情,我是刑警寧澤总滩,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布纲堵,位于F島的核電站,受9級特大地震影響闰渔,放射性物質(zhì)發(fā)生泄漏席函。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一冈涧、第九天 我趴在偏房一處隱蔽的房頂上張望茂附。 院中可真熱鬧,春花似錦督弓、人聲如沸营曼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蒂阱。三九已至,卻和暖如春狂塘,著一層夾襖步出監(jiān)牢的瞬間录煤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工荞胡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留妈踊,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓泪漂,卻偏偏與公主長得像廊营,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子窖梁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容