RxSwift + Moya + ObjectMapper
https://www.jianshu.com/p/173915b943af
use_frameworks!
target 'RXDemo' do
pod 'RxSwift'
pod 'RxCocoa'
pod 'Moya-ObjectMapper/RxSwift'
pod 'Moya/RxSwift'
end
import Moya
let DouBanProvider = MoyaProvider<DouBanAPI>()
public enum DouBanAPI {
case channels //获取频道列表
case playlist(String) //获取歌曲
}
extension DouBanAPI: TargetType {
public var baseURL: URL {
switch self {
case .channels:
return URL(string: "https://www.douban.com")!
case .playlist(_):
return URL(string: "https://douban.fm")!
}
}
public var path: String {
switch self {
case .channels:
return "/j/app/radio/channels"
case .playlist(_):
return "/j/mine/playlist"
}
}
public var method: Moya.Method {
return .get
}
public var task: Task {
switch self {
case .playlist(let channel):
var params: [String: Any] = [:]
params["channel"] = channel
params["type"] = "n"
params["from"] = "mainsite"
return .requestParameters(parameters: params,
encoding: URLEncoding.default)
default:
return .requestPlain
}
}
public var validate: Bool {
return false
}
public var sampleData: Data {
return "{}".data(using: String.Encoding.utf8)!
}
public var headers: [String: String]? {
return nil
}
}
import UIKit
import ObjectMapper
//豆瓣接口模型
struct Douban: Mappable {
//频道列表
var channels: [Channel]?
init?(map: Map) { }
// Mappable
mutating func mapping(map: Map) {
channels <- map["channels"]
}
}
//频道模型
struct Channel: Mappable {
var name: String?
var nameEn:String?
var channelId: String?
var seqId: Int?
var abbrEn: String?
init?(map: Map) { }
// Mappable
mutating func mapping(map: Map) {
name <- map["name"]
nameEn <- map["name_en"]
channelId <- map["channel_id"]
seqId <- map["seq_id"]
abbrEn <- map["abbr_en"]
}
}
//歌曲列表模型
struct Playlist: Mappable {
var r: Int!
var isShowQuickStart: Int!
var song:[Song]!
init?(map: Map) { }
// Mappable
mutating func mapping(map: Map) {
r <- map["r"]
isShowQuickStart <- map["is_show_quick_start"]
song <- map["song"]
}
}
//歌曲模型
struct Song: Mappable {
var title: String!
var artist: String!
init?(map: Map) { }
// Mappable
mutating func mapping(map: Map) {
title <- map["title"]
artist <- map["artist"]
}
}
let data = DouBanProvider.rx.request(.channels)
.mapObject(Douban.self)
.map { $0.channels ?? [] }
.asObservable()
data.bind(to: self.tableView.rx.items) { tableView, _, channel in
let cell = tableView.dequeueReusableCell(withIdentifier: "CellIdentifier")!
cell.textLabel?.text = channel.name
cell.accessoryType = .disclosureIndicator
return cell
}.disposed(by: self.disposeBag)
tableView.rx.modelSelected(Channel.self)
.map{ $0.channelId ?? "" }
.flatMap { DouBanProvider.rx.request(.playlist($0)) }
.mapObject(Playlist.self)
.subscribe(onNext: { [weak self] (playList) in
if playList.song != nil && playList.song.count > 0 {
let artist = playList.song[0].artist!
let title = playList.song[0].title!
let message = "歌手:\(artist)\n歌曲:\(title)"
self?.showAlert(title: "歌曲信息", message: message)
}
}).disposed(by: self.disposeBag)
import RxSwift
import RxCocoa
import ObjectMapper
class DoubanService {
//获取频道数据
func loadChannels() -> Observable<[Channel]> {
return DouBanProvider.rx.request(.channels)
.mapObject(Douban.self)
.map{ $0.channels ?? [] }
.asObservable()
}
//获取歌曲列表数据
func loadPlaylist(channelId: String) -> Observable<Playlist> {
return DouBanProvider.rx.request(.playlist(channelId))
.mapObject(Playlist.self)
.asObservable()
}
//获取频道下第一首歌曲
func loadFirstSong(channelId: String) -> Observable<Song> {
return loadPlaylist(channelId: channelId)
.filter{ $0.song != nil && $0.song.count > 0}
.map{ $0.song[0] }
}
}
import UIKit
import RxSwift
import RxCocoa
class MusicViewController: UIViewController {
private var viewModel = MusicListViewModel()
private var disposeBag = DisposeBag()
private lazy var tableView: UITableView = {
let tableView = UITableView(frame: self.view.bounds)
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "CellIdentifier")
tableView.tableFooterView = UIView()
return tableView
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = .white
self.view.addSubview(self.tableView)
let service = DoubanService()
//获取列表数据
let data = service.loadChannels()
//将数据绑定到表格
data.bind(to: tableView.rx.items) { (tableView, row, element) in
let cell = tableView.dequeueReusableCell(withIdentifier: "CellIdentifier")!
cell.textLabel?.text = "\(element.name!)"
cell.accessoryType = .disclosureIndicator
return cell
}.disposed(by: disposeBag)
//单元格点击
tableView.rx.modelSelected(Channel.self)
.map{ $0.channelId ?? "" }
.flatMap(service.loadFirstSong)
.subscribe(onNext: {[weak self] song in
//将歌曲信息弹出显示
let message = "歌手:\(song.artist!)\n歌曲:\(song.title!)"
self?.showAlert(title: "歌曲信息", message: message)
}).disposed(by: disposeBag)
}
//显示消息
func showAlert(title:String, message:String) {
let alertController = UIAlertController(title: title,
message: message, preferredStyle: .alert)
let cancelAction = UIAlertAction(title: "确定", style: .cancel, handler: nil)
alertController.addAction(cancelAction)
self.present(alertController, animated: true, completion: nil)
}
}
import UIKit
import SnapKit
class ViewController: UIViewController {
private lazy var items = [HomeDataItem]()
private lazy var availableTexts = [String]()
private lazy var availableImages = [String]()
private lazy var tableView: UITableView = {
let tableView = UITableView()
tableView.backgroundColor = .clear
tableView.dataSource = self
tableView.delegate = self
tableView.rowHeight = 50
tableView.register(HomeTableViewCell.self, forCellReuseIdentifier: "Cell")
return tableView
}()
private lazy var deleteButton : UIButton = {
var deleteButton = UIButton()
deleteButton.setTitle("Delete", for: .normal)
deleteButton.setTitle("", for: .disabled)
deleteButton.backgroundColor = .red
deleteButton.layer.cornerRadius = 10
deleteButton.tintColor = .white
deleteButton.addTarget(self, action: #selector(deleteAction), for: .touchUpInside)
return deleteButton
}()
private lazy var addButton : UIButton = {
var addButton = UIButton()
addButton.setTitle("Add", for: .normal)
addButton.setTitle("", for: .disabled)
addButton.backgroundColor = .blue
addButton.layer.cornerRadius = 10
addButton.tintColor = .white
addButton.addTarget(self, action: #selector(addAction), for: .touchUpInside)
return addButton
}()
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
createDataSources()
}
private func setupUI() {
let imageView = UIImageView(image: UIImage(named: "bg"))
imageView.contentMode = .scaleAspectFill
view.addSubview(imageView)
imageView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
view.addSubview(tableView)
tableView.snp.makeConstraints { make in
make.left.top.right.equalToSuperview()
make.bottom.equalToSuperview().offset(self.view.safeAreaInsets.bottom).offset(-100)
}
view.addSubview(deleteButton)
deleteButton.snp.makeConstraints { make in
make.right.equalToSuperview().offset(-20)
make.top.equalTo(tableView.snp.bottom).offset(10)
make.size.equalTo(CGSize(width: 100, height: 40))
}
view.addSubview(addButton)
addButton.snp.makeConstraints { make in
make.left.equalToSuperview().offset(10)
make.top.equalTo(tableView.snp.bottom).offset(10)
make.size.equalTo(CGSize(width: 100, height: 40))
}
}
private func createDataSources() {
getImages()
readTxtContent()
initializeData()
}
private func initializeData() {
for _ in 1...10 {
addItem()
}
}
private func addItem() {
if let item = addRandomItem() {
items.append(item)
updateButtonStates()
}
}
private func addRandomItem() -> HomeDataItem? {
if availableImages.isEmpty || availableTexts.isEmpty {
return nil
}
let randomImageIndex = Int.random(in: 0..<availableImages.count)
let randomTextIndex = Int.random(in: 0..<availableTexts.count)
let image = availableImages.remove(at: randomImageIndex)
let text = availableTexts.remove(at: randomTextIndex)
return HomeDataItem(imageName: image, title: text)
}
@objc private func deleteAction() {
if items.count >= 2 {
deleteItem(at: 1)
} else if items.count == 1 {
deleteItem(at: 0)
}
}
@objc private func addAction() {
if items.count < 3 {
addItem()
self.tableView.reloadData()
} else if items.count < 20 {
if let item = addRandomItem() {
items.insert(item, at: 2)
tableView.insertRows(at: [IndexPath(row: 2, section: 0)], with: .bottom)
}
if (self.items.count == 20) {
addButton.isEnabled = false
}
}
}
private func updateButtonStates() {
deleteButton.isEnabled = items.count > 0
addButton.isEnabled = items.count < 20
}
private func deleteItem(at index: Int) {
guard index < items.count else { return }
let item = items.remove(at: index)
availableImages.append(item.imageName)
availableTexts.append(item.title)
tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .fade)
updateButtonStates()
}
private func getImages() {
for i in 1...25 {
let name = String(format: "thumb%02d", i)
availableImages.append(name)
}
}
private func readTxtContent() {
guard let path = Bundle.main.path(forResource: "strings", ofType: "txt") else {
print("File not found.")
return
}
do {
let content = try String(contentsOfFile: path, encoding: .utf8)
availableTexts = content.split(separator: "\n").map { String($0.trimmingCharacters(in: .whitespaces)) }
} catch {
print("Error reading file: \(error)")
}
}
}
extension ViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print("Selected item: \(items[indexPath.row])")
}
}
extension ViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! HomeTableViewCell
cell.model = items[indexPath.row]
cell.selectionStyle = .none
return cell
}
}
class HomeTableViewCell: UITableViewCell {
var model: HomeDataItem? {
didSet {
if let model = model {
avatarImageView.image = UIImage(named: model.imageName)
titleLabel.text = model.title
}
}
}
private let maskLayer = CALayer()
private lazy var avatarImageView: UIImageView = {
let imageView = UIImageView()
imageView.contentMode = .scaleAspectFit
return imageView
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 15)
label.textColor = .white
return label
}()
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
maskLayer.frame = CGRect(x: 0, y: 0, width: 40, height: 40)
if let maskImage = UIImage(named: "frame") {
maskLayer.contents = maskImage.cgImage
}
self.avatarImageView.layer.mask = maskLayer
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
self.contentView.backgroundColor = .clear
self.backgroundColor = .clear
contentView.addSubview(avatarImageView)
avatarImageView.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalToSuperview().offset(15)
make.size.equalTo(40)
}
contentView.addSubview(titleLabel)
titleLabel.snp.makeConstraints { make in
make.centerY.equalToSuperview()
make.left.equalTo(avatarImageView.snp.right).offset(10)
}
}
}