iOS项目案例,采用MVVM架构,实现一个简单的电影列表应用,包含网络请求获取电影数据、数据展示、以及点击电影项查看详情的功能。该案例使用了Alamofire进行网络请求,RxSwift进行数据绑定和响应式编程。
1. 首先,创建一个新的iOS项目,并导入所需的第三方库:Alamofire和RxSwift(可以通过CocoaPods或Swift Package Manager导入)。
2. 创建相关文件:
• Movie.swift:定义电影数据模型。
• MovieAPI.swift:负责网络请求获取电影数据。
• MovieListViewModel.swift:视图模型,处理电影列表相关的业务逻辑。
• MovieDetailViewModel.swift:视图模型,处理电影详情相关的业务逻辑。
• MovieListViewController.swift:视图控制器,展示电影列表。
• MovieDetailViewController.swift:视图控制器,展示电影详情。
3. Movie.swift代码如下:
import Foundation
struct Movie {
let title: String
let overview: String
let posterPath: String
let releaseDate: String
}
4. MovieAPI.swift代码如下:
import Foundation
import Alamofire
class MovieAPI {
static func fetchPopularMovies(completion: @escaping (Result<[Movie], Error>) -> Void) {
let url = "https://api.themoviedb.org/3/movie/popular"
let parameters: [String: Any] = [
"api_key": "YOUR_API_KEY", // 这里需要替换为你自己的TheMovieDB API Key
"language": "en-US"
]
AF.request(url, method:.get, parameters: parameters).responseJSON { response in
if let error = response.error {
completion(.failure(error))
return
}
guard let data = response.data else {
completion(.failure(NSError(domain: "MovieAPI", code: -1, userInfo: nil)))
return
}
do {
let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
guard let results = json?["results"] as? [[String: Any]] else {
completion(.failure(NSError(domain: "MovieAPI", code: -2, userInfo: nil)))
return
}
let movies = results.compactMap { dict in
guard let title = dict["title"] as? String,
let overview = dict["overview"] as? String,
let posterPath = dict["poster_path"] as? String,
let releaseDate = dict["release_date"] as? String else {
return nil
}
return Movie(title: title, overview: overview, posterPath: posterPath, releaseDate: releaseDate)
}
completion(.success(movies))
} catch {
completion(.failure(error))
}
}
}
}
5. MovieListViewModel.swift代码如下:
import Foundation
import RxSwift
import RxCocoa
class MovieListViewModel {
let movies = BehaviorSubject<[Movie]>(value: [])
let isLoading = BehaviorSubject<Bool>(value: false)
let error = PublishSubject<Error>()
private let disposeBag = DisposeBag()
init() {
loadMovies()
}
func loadMovies() {
isLoading.onNext(true)
MovieAPI.fetchPopularMovies { [weak self] result in
self?.isLoading.onNext(false)
switch result {
case.success(let movies):
self?.movies.onNext(movies)
case.failure(let error):
self?.error.onNext(error)
}
}
}
func transformToDetailViewModel(for movie: Movie) -> MovieDetailViewModel {
return MovieDetailViewModel(movie: movie)
}
}
6. MovieDetailViewModel.swift代码如下:
import Foundation
class MovieDetailViewModel {
let movie: Movie
init(movie: Movie) {
self.movie = movie
}
var title: String {
return movie.title
}
var overview: String {
return movie.overview
}
var releaseDate: String {
return movie.releaseDate
}
var posterURL: String {
return "https://image.tmdb.org/t/p/w500\(movie.posterPath)"
}
}
7. MovieListViewController.swift代码如下:
import UIKit
import RxSwift
import RxCocoa
import Alamofire
class MovieListViewController: UIViewController {
@IBOutlet weak var tableView: UITableView!
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
@IBOutlet weak var errorLabel: UILabel!
private let disposeBag = DisposeBag()
private var viewModel: MovieListViewModel!
override func viewDidLoad() {
super.viewDidLoad()
viewModel = MovieListViewModel()
// 绑定加载状态到ActivityIndicator
viewModel.isLoading
.bind(to: activityIndicator.rx.isAnimating)
.disposed(by: disposeBag)
// 绑定错误信息到Label
viewModel.error
.map { $0.localizedDescription }
.bind(to: errorLabel.rx.text)
.disposed(by: disposeBag)
// 注册Cell
tableView.register(UINib(nibName: "MovieCell", bundle: nil), forCellReuseIdentifier: "MovieCell")
// 绑定电影数据到TableView
viewModel.movies
.bind(to: tableView.rx.items(cellIdentifier: "MovieCell", cellType: MovieCell.self)) { (row, movie, cell) in
cell.configure(with: movie)
}
.disposed(by: disposeBag)
// 处理Cell点击事件
tableView.rx.modelSelected(Movie.self)
.subscribe(onNext: { [weak self] movie in
let detailViewModel = self?.viewModel.transformToDetailViewModel(for: movie)
let detailVC = MovieDetailViewController(viewModel: detailViewModel!)
self?.navigationController?.pushViewController(detailVC, animated: true)
})
.disposed(by: disposeBag)
}
}
8. 创建一个自定义的UITableViewCell类MovieCell.swift:
import UIKit
class MovieCell: UITableViewCell {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var overviewLabel: UILabel!
func configure(with movie: Movie) {
titleLabel.text = movie.title
overviewLabel.text = movie.overview
}
}
9. MovieDetailViewController.swift代码如下:
import UIKit
class MovieDetailViewController: UIViewController {
@IBOutlet weak var titleLabel: UILabel!
@IBOutlet weak var overviewLabel: UILabel!
@IBOutlet weak var releaseDateLabel: UILabel!
@IBOutlet weak var posterImageView: UIImageView!
private var viewModel: MovieDetailViewModel!
init(viewModel: MovieDetailViewModel) {
self.viewModel = viewModel
super.init(nibName: "MovieDetailViewController", bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
titleLabel.text = viewModel.title
overviewLabel.text = viewModel.overview
releaseDateLabel.text = viewModel.releaseDate
if let url = URL(string: viewModel.posterURL) {
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let data = data, let image = UIImage(data: data) {
DispatchQueue.main.async {
self.posterImageView.image = image
}
}
}
task.resume()
}
}
}
在这个案例中:
• Movie结构体定义了电影的数据模型。
• MovieAPI负责从TheMovieDB API获取热门电影数据。
• MovieListViewModel处理电影列表的业务逻辑,包括数据加载、状态管理和错误处理,并提供方法转换到电影详情的视图模型。
• MovieDetailViewModel处理电影详情的相关逻辑,将电影数据转换为视图可直接使用的格式。
• MovieListViewController和MovieDetailViewController作为视图控制器,负责视图的展示和与视图模型的交互,通过RxSwift实现数据的绑定和事件的处理。












网友评论