RXSwift — 使用MVVM實(shí)現(xiàn)一個(gè)歌單列表

今天我們將用RxSwift加上MVVM設(shè)計(jì)模式來(lái)開(kāi)發(fā)一個(gè)簡(jiǎn)單的小Demo德迹,在UICollectionView和UITableView中顯示林肯公園的專(zhuān)輯和歌曲列表。Let's go!

UI 部分

第一步双吆,用CollectionView搭建專(zhuān)輯九宮格,用TableView搭建歌曲列表会前。

這兩部分我們可以分成兩個(gè)控制器來(lái)做好乐,主要為了能重復(fù)利用,然后使用 childViewController 來(lái)添加瓦宜。
我們的主控制器就劃分為兩個(gè)控制器:

  1. AlbumCollectionViewVC
  2. TrackTableViewVC

所以我們的主控制器就會(huì)像這樣:

第二步蔚万,使用nib創(chuàng)建cells,以便后面能夠重用它們:

記得在AlbumCollectionViewVCviewDidLoad方法中注冊(cè)nib文件:

//register 'AlbumsCollectionViewCell' to UICollectionView
albumsCollectionView.register(UINib(nibName: "AlbumsCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: String(describing: AlbumsCollectionViewCell.self))

第三步临庇,關(guān)聯(lián)兩個(gè)view作為AlbumCollectionViewVC和TrackTableViewVC的容器view反璃。

@IBOutlet weak var albumsVCView: UIView!
    
    private lazy var albumsViewController: AlbumsCollectionViewVC = {
        // Load Storyboard
        let storyboard = UIStoryboard(name: "Home", bundle: Bundle.main)
        
        // Instantiate View Controller
        var viewController = storyboard.instantiateViewController(withIdentifier: "AlbumsCollectionViewVC") as! AlbumsCollectionViewVC
        
        // Add View Controller as Child View Controller
        self.add(asChildViewController: viewController, to: albumsVCView)
        
        return viewController
    }()

我們已經(jīng)把視圖部分搭建好了,接下來(lái)創(chuàng)建ViewModel假夺。

View Model 部分

我們創(chuàng)建一個(gè)HomeViewModel類(lèi)淮蜈,在該類(lèi)的作用是:

  1. 從服務(wù)器獲取數(shù)據(jù),并按照視圖需要展現(xiàn)的方式來(lái)解析請(qǐng)求到的數(shù)據(jù)已卷。
  2. 將解析到的數(shù)據(jù)傳遞給父類(lèi)控制器梧田,父類(lèi)控制器將這些數(shù)據(jù)再傳遞給子視圖控制器。

為了更好地理解,請(qǐng)看下面的圖表:

所以整個(gè)過(guò)程就是:父類(lèi)控制器從它的視圖模型請(qǐng)求數(shù)據(jù)柿扣,視圖模型向網(wǎng)絡(luò)層發(fā)送請(qǐng)求肖方。然后視圖模型解析數(shù)據(jù)并將其提供給父類(lèi)控制器。

HomeViewModel中還提供了以下幾個(gè)屬性:

  1. Loading(Bool):當(dāng)我們向服務(wù)器發(fā)送請(qǐng)求時(shí)未状,我們應(yīng)該顯示Loading提示彈框以示加載中俯画。這樣用戶(hù)就明白,有些東西正在加載司草。為此艰垂,我們需要定義一個(gè)PublishSubject<Bool>類(lèi)型的可觀察對(duì)象。當(dāng)它為true時(shí)埋虹,它將意味著正在加載猜憎,當(dāng)它為false時(shí),意味著已經(jīng)加載完成搔课。
  2. Error(homeError):來(lái)自服務(wù)器的錯(cuò)誤和任何其他錯(cuò)誤胰柑。如果它有值,我們將它在屏幕上顯示出來(lái)爬泥。
  3. albums和tracks:集合和表視圖數(shù)據(jù)柬讨。
public enum HomeError {
        case internetError(String)
        case serverMessage(String)
}
    
public let albums : PublishSubject<[Album]> = PublishSubject()
public let tracks : PublishSubject<[Track]> = PublishSubject()
public let loading: PublishSubject<Bool> = PublishSubject()
public let error : PublishSubject<HomeError> = PublishSubject()

上面四個(gè)屬性都是用PublishSubject定義的observable對(duì)象,問(wèn)題來(lái)了袍啡,PublishSubject是什么踩官,怎么用?

Subjects 和 PublishSubject

再了解PublishSubject之前境输,先來(lái)聊聊Subjects蔗牡。

Subjects既是可觀察的observable對(duì)象又是observer觀察者。它們可以接收事件嗅剖,也可以訂閱辩越。subject接收.next事件,并且每次接收到一個(gè)事件信粮,它都會(huì)返回并將其發(fā)送給它的訂閱者黔攒。

RxSwift有4種subject類(lèi)型:

  • PublishSubject。
  • BehaviorSubject蒋院。
  • ReplaySubject。
  • Variable莲绰。

PublishSubject是Subject的一種欺旧。我們用一張圖來(lái)展示一下:

從圖中看出,如果在事件1之后訂閱蛤签,我們就不能接收到事件1辞友,而可以接收到事件2和事件3。如果在事件2之后訂閱,那么我們只能接收到事件3称龙,而接收不到事件1和事件2留拾。

PublishSubject 如何使用?

我們來(lái)舉個(gè)例子鲫尊。

第一步痴柔,創(chuàng)建一個(gè)類(lèi)型為String的PublishSubject:

let subject = PublishSubject<String>()

第二步,通過(guò)onNext發(fā)出事件:

subject.onNext(“No event emitted??”)

此時(shí)不會(huì)有任何打印疫向,因?yàn)闆](méi)有人訂閱這個(gè)它咳蔚。

第三步,我們來(lái)訂閱它:

let subscriptionOne = subject
                          .subscribe(onNext: { string in
                              print("First Subscription: ", string)
                          })

第四步搔驼,使用subject通過(guò)onNext發(fā)出事件:

subject.onNext("1")
subject.onNext("2")

只輸出了訂閱后的事件數(shù)據(jù):

First Subscription:  1
First Subscription:  2

第五步谈火,再創(chuàng)建另一個(gè)訂閱:

let subscriptionTwo = subject
                         .subscribe({ (event) in
                             print("Second Subscription: \(event)"))
                          })

第六步,并發(fā)射事件:

subject.onNext("3")

這是在subscription1和subscription2被發(fā)出后訂閱的舌涨,所以此時(shí)的訂閱者只能監(jiān)聽(tīng)到事件3糯耍,又增加了以下的打印的結(jié)果:

First Subscription:  3
Second Subscription: next(3)

第七步,我們嘗試釋放掉subscriptionOne并發(fā)出事件4:

subscriptionOne.dispose()
subject.onNext("4")

這時(shí)只有觀察者subscriptionTwo可以監(jiān)聽(tīng)事件囊嘉,并執(zhí)行打印操作温技,因?yàn)閟ubscriptionOne資源已被釋放。

Second Subscription: next(4)

第八步哗伯,嘗試發(fā)出complete event荒揣,同時(shí)釋放掉subscriptionTwo。

subject.onCompleted()
subscriptionTwo.dispose()
subject.onNext("Any event emitted??")

Complete event 會(huì)被打印出來(lái):

Second Subscription: completed

整個(gè)代碼如下:

let subject = PublishSubject<String>()
subject.onNext(“No event emitted??”)

let subscriptionOne = subject
                          .subscribe(onNext: { string in
                              print("First Subscription: ", string)
                          })

subject.onNext("1")
subject.onNext("2")

let subscriptionTwo = subject
                         .subscribe({ (event) in
                             print("Second Subscription: \(event)"))
                          })

subject.onNext("3")
subscriptionOne.dispose()

subject.onNext("4")
subject.onCompleted()

subscriptionTwo.dispose()
subject.onNext("Any event emitted??")

整個(gè)輸出如下:

First Subscription:  1
First Subscription:  2
First Subscription:  3
Second Subscription: next(3)
Second Subscription: next(4)
Second Subscription: completed

PublishSubject相當(dāng)于熱信號(hào)焊刹,只會(huì)接收到后面的事件系任。我們這個(gè)Demo只會(huì)用到PublishSubject,也是因?yàn)樗某跏蓟挥媒o初始值虐块,比較方便俩滥。

將數(shù)據(jù)綁定到UI上

現(xiàn)在,我們來(lái)看看如何給我們的視圖提供數(shù)據(jù)贺奠。

首先霜旧,我們將homeViewModel中的loading綁定給HomeVC 的isAnimating,這意味著每當(dāng)viewModel的loading值發(fā)生改變時(shí)儡率,視圖控制器的isAnimating值也會(huì)同時(shí)改變挂据。

 homeViewModel.loading
            .bind(to: self.rx.isAnimating).disposed(by: disposeBag)

看到上面的.rx了嗎?類(lèi)似的我們可以通過(guò).rx的形式訪(fǎng)問(wèn)到很多視圖的屬性儿普,從而能夠?qū)?shù)據(jù)綁定到UIKit中崎逃。

但是要注意了,對(duì)于自定義的屬性眉孩,RxCocoa并不支持.rx个绍,我們可以使用拓展來(lái)讓它支持:

extension Reactive where Base: UIViewController {
    
    /// Bindable sink for `startAnimating()`, `stopAnimating()` methods.
    public var isAnimating: Binder<Bool> {
        return Binder(self.base, binding: { (vc, active) in
            if active {
                vc.startAnimating()
            } else {
                vc.stopAnimating()
            }
        })
    }
}

我解釋一下上面的代碼:

  1. 對(duì)Reactive進(jìn)行了擴(kuò)展勒葱,目的想對(duì)自定義的屬性進(jìn)行.rx調(diào)用。
  2. 設(shè)置isAnimating變量的類(lèi)型為Binder<Bool>巴柿,并對(duì)它進(jìn)行實(shí)現(xiàn)凛虽。
  3. 實(shí)現(xiàn)代碼里,返回了一個(gè)Binder广恢,self.base就是viewController本身凯旋,后面的閉包提供兩個(gè)參數(shù):視圖控制器(vc)和isAnimating值(active)。如果active為true袁波,我們就用vc.startAnimating()顯示正在加載動(dòng)畫(huà)瓦阐,如果active為false,就用vc.stopAnimating()隱藏加載動(dòng)畫(huà)篷牌。

現(xiàn)在我們的loading已經(jīng)準(zhǔn)備好了接收來(lái)自ViewModel的數(shù)據(jù)睡蟋。讓我們來(lái)看看其他的binders:

// observing errors to show
        
        homeViewModel
            .error
            .observeOn(MainScheduler.instance)
            .subscribe(onNext: { (error) in
                switch error {
                case .internetError(let message):
                    MessageView.sharedInstance.showOnView(message: message, theme: .error)
                case .serverMessage(let message):
                    MessageView.sharedInstance.showOnView(message: message, theme: .warning)
                }
            })
            .disposed(by: disposeBag)

在上面的代碼中,每當(dāng)有來(lái)自ViewModel的錯(cuò)誤出現(xiàn)時(shí)枷颊,我們都會(huì)訂閱到它戳杀。我們可以對(duì)監(jiān)聽(tīng)到的錯(cuò)誤進(jìn)行進(jìn)一步處理,比如顯示一個(gè)彈出窗口什么的夭苗。

那么.observeOn(MainScheduler.instance)又是什么呢信卡?它的作用是回到主線(xiàn)程發(fā)送錯(cuò)誤信號(hào)。

綁定Albums 和 Tracks

現(xiàn)在我們將Albums 和 Tracks分別綁定給UICollectionView和UITableView:

 // binding albums to album container
        
        homeViewModel
            .albums
            .observeOn(MainScheduler.instance)
            .bind(to: albumsViewController.albums)
            .disposed(by: disposeBag)
        
        // binding tracks to track container
        
        homeViewModel
            .tracks
            .observeOn(MainScheduler.instance)
            .bind(to: tracksViewController.tracks)
            .disposed(by: disposeBag)

數(shù)據(jù)請(qǐng)求

現(xiàn)在我們?cè)倩氐絍iewModel中题造,編寫(xiě)數(shù)據(jù)請(qǐng)求部分:

public func requestData(){
        
        self.loading.onNext(true)
        APIManager.requestData(url: requestUrl, method: .get, parameters: nil, completion: { (result) in
            self.loading.onNext(false)
            switch result {
            case .success(let returnJson) :
                let albums = returnJson["Albums"].arrayValue.compactMap {return Album(data: try! $0.rawData())}
                let tracks = returnJson["Tracks"].arrayValue.compactMap {return Track(data: try! $0.rawData())}
                self.albums.onNext(albums)
                self.tracks.onNext(tracks)
            case .failure(let failure) :
                switch failure {
                case .connectionError:
                    self.error.onNext(.internetError("Check your Internet connection."))
                case .authorizationError(let errorJson):
                    self.error.onNext(.serverMessage(errorJson["message"].stringValue))
                default:
                    self.error.onNext(.serverMessage("Unknown Error"))
                }
            }
        })
        
    }
  1. 首先我們對(duì)loading發(fā)送一個(gè)true值傍菇,因?yàn)槲覀円呀?jīng)在HomeVC類(lèi)中綁定了loading,這樣我們的viewController就會(huì)顯示正在加載動(dòng)畫(huà)界赔。
  2. 接下來(lái)丢习,發(fā)送一個(gè)數(shù)據(jù)請(qǐng)求,得到響應(yīng)后淮悼,在回調(diào)閉包里我們應(yīng)該結(jié)束加載動(dòng)畫(huà)咐低,所以又發(fā)送了一個(gè)false值。
  3. 對(duì)于請(qǐng)求結(jié)果result我們做了區(qū)分袜腥,如果為success见擦,就解析數(shù)據(jù)并發(fā)出albums和albums的值,如果為false羹令,就會(huì)發(fā)出錯(cuò)誤值鲤屡,根據(jù)錯(cuò)誤不同的情況我們?cè)僮鱿鄳?yīng)的處理。

現(xiàn)在我們的數(shù)據(jù)準(zhǔn)備好了福侈,我們傳遞給了我們的子視圖控制器酒来,最后我們應(yīng)該在CollectionView和TableView中顯示數(shù)據(jù)了。
TracksTableViewVC中:

public var tracks = PublishSubject<[Track]>()

AlbumsCollectionViewVC中:

public var albums = PublishSubject<[Album]>()

現(xiàn)在在trackTableViewVC的ViewDidLoad方法里癌刽,將數(shù)據(jù)源tracks綁定給UITableView:

tracks.bind(to: tracksTableView.rx.items(cellIdentifier: "TracksTableViewCell", cellType: TracksTableViewCell.self)) {  (row,track,cell) in
            cell.cellTrack = track
            }.disposed(by: disposeBag)

僅僅兩行代碼就搞定了役首,不需要想以前那樣設(shè)置delegate和datasource,也不用寫(xiě)一大堆代理方法显拜,RxCocoa搞定了一切衡奥!

public var cellTrack : Track! {
        didSet {
            self.trackImage.clipsToBounds = true
            self.trackImage.layer.cornerRadius = 3
            self.trackImage.loadImage(fromURL: cellTrack.trackArtWork)
            self.trackTitle.text = cellTrack.name
            self.trackArtist.text = cellTrack.artist
        }
    }

給tableView 和 collectionView 添加動(dòng)畫(huà):

// animation to cells
tracksTableView.rx.willDisplayCell
            .subscribe(onNext: ({ (cell,indexPath) in
                cell.alpha = 0
                let transform = CATransform3DTranslate(CATransform3DIdentity, -250, 0, 0)
                cell.layer.transform = transform
                UIView.animate(withDuration: 1, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.5, options: .curveEaseOut, animations: {
                    cell.alpha = 1
                    cell.layer.transform = CATransform3DIdentity
                }, completion: nil)
            })).disposed(by: disposeBag)

最后完成效果:

小結(jié)

我們使用RxSwift和MVVM實(shí)現(xiàn)了一個(gè)小Demo,其中只是涉及到部分概念远荠,后續(xù)會(huì)通過(guò)實(shí)踐來(lái)學(xué)習(xí)其他的用法矮固。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市譬淳,隨后出現(xiàn)的幾起案子档址,更是在濱河造成了極大的恐慌,老刑警劉巖邻梆,帶你破解...
    沈念sama閱讀 218,640評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件守伸,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡浦妄,警方通過(guò)查閱死者的電腦和手機(jī)尼摹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)剂娄,“玉大人蠢涝,你說(shuō)我怎么就攤上這事≡呐常” “怎么了和二?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,011評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)耳胎。 經(jīng)常有香客問(wèn)我惯吕,道長(zhǎng),這世上最難降的妖魔是什么场晶? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,755評(píng)論 1 294
  • 正文 為了忘掉前任混埠,我火速辦了婚禮,結(jié)果婚禮上诗轻,老公的妹妹穿的比我還像新娘钳宪。我一直安慰自己,他們只是感情好扳炬,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,774評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布吏颖。 她就那樣靜靜地躺著,像睡著了一般恨樟。 火紅的嫁衣襯著肌膚如雪半醉。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,610評(píng)論 1 305
  • 那天劝术,我揣著相機(jī)與錄音缩多,去河邊找鬼呆奕。 笑死,一個(gè)胖子當(dāng)著我的面吹牛衬吆,可吹牛的內(nèi)容都是我干的梁钾。 我是一名探鬼主播,決...
    沈念sama閱讀 40,352評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼逊抡,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼姆泻!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起冒嫡,我...
    開(kāi)封第一講書(shū)人閱讀 39,257評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤拇勃,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后孝凌,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體方咆,經(jīng)...
    沈念sama閱讀 45,717評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,894評(píng)論 3 336
  • 正文 我和宋清朗相戀三年蟀架,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了峻呛。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,021評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡辜窑,死狀恐怖钩述,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情穆碎,我是刑警寧澤牙勘,帶...
    沈念sama閱讀 35,735評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站所禀,受9級(jí)特大地震影響方面,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜色徘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,354評(píng)論 3 330
  • 文/蒙蒙 一恭金、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧褂策,春花似錦横腿、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,936評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至遍搞,卻和暖如春罗侯,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背溪猿。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,054評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工纫塌, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人讲弄。 一個(gè)月前我還...
    沈念sama閱讀 48,224評(píng)論 3 371
  • 正文 我出身青樓护戳,卻偏偏與公主長(zhǎng)得像缴渊,于是被迫代替她去往敵國(guó)和親蝌借。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,974評(píng)論 2 355