在View (Swift)中传递一个模型



我正在创建一个基本的iOS应用程序,我从包含图像的API获得数据数组(JSON)。我使用的是UIViewController,并有一个自定义的UITableViewCell

为了停止发送网络请求,我创建了一个存储<url: UIImage>的缓存。问题是为了将图像添加到缓存中,我将模型(网络)类传递给自定义UITableViewCell(视图)。

我的理解是视图应该不知道模型。这是一个可接受的设计MVC或有一种方法来解决这个问题吗?

下面是抽象代码(现在使用字典作为缓存)。稍后将更改为NSCache)我还使用协议/委托来传递网络和视图控制器之间的日期。
protocol ModelDelegate {

func dataFetched(_ data:[DataItem])
}
//This is the model(networking) class
class Model {

var delegate: ModelDelegate?

var imagecache: [String: UIImage] = [:]

func getData() {
let url = URL(string: Constants.DATA_URL)

guard url != nil else {
print("Invalid URL!")
return
}

let session = URLSession.shared.dataTask(with: url!) { data, response, error in

if error != nil && data == nil {
print("There was a data retriving error!")
return
}

let decoder = JSONDecoder()
do {
let response = try decoder.decode(Data.self, from: data!)

if response.data != nil {

let sorteddata = response.data!.sorted(by: { $0.data < $1.data })


DispatchQueue.main.async {
self.delegate?.dataFetched(sorteddata)
}

}

} catch  {
print(error.localizedDescription)
}
}
session.resume()
}
}
//Custom UITableView Cell
class DataViewCell: UITableViewCell {

@IBOutlet weak var dataImage: UIImageView!
@IBOutlet weak var dataName: UILabel!

var model = Model()
var data: DataItem?

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
}

func setCell(_ data: DataItem) {
self.data = data

self.dataName.text = data.strdata

guard self.data?.thumbnail != nil else {
return
}

if let imageData = model.imagecache[self.data!.thumbnail] {
print("using cache")
DispatchQueue.main.async {
self.DataImage.image = imageData
}  
}

let url = URL(string: self.data!.thumbnail)

let session = URLSession.shared.dataTask(with: url!) { data, response, error in

if error == nil && data != nil {


let image = UIImage(data: data!)
self.model.imagecache[self.data!.thumbnail] = image
DispatchQueue.main.async {
self.DataImage.image = image
}

}
}

session.resume()
}
}
//View Controller
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, ModelDelegate {
@IBOutlet weak var tableView: UITableView!

var model = Model()
var data = [DataItem]()
override func viewDidLoad() {
super.viewDidLoad()

tableView.dataSource = self
tableView.delegate = self

model.delegate = self

model.getdata()
}

func dataFetched(_ data: [DataItem]) {
self.data = data

tableView.reloadData()
}

//TableView methods

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

let cell = tableView.dequeueReusableCell(withIdentifier: Constants.ID, for: indexPath) as! DataViewCell
let data = self.categories[indexPath.row]
cell.setCell(data)

return cell  
}
}

单元格是一个视图,视图不应该与网络层或底层数据模型交互。很难知道你在做什么,因为你的代码不编译,所以我编了一个例子:

首先你有一个名为Model的类,它不是一个真正的模型。它是一个服务或数据提供者。你的模型是DataItem

我的模特通常是这样的:

struct DemoModel: Codable {
let name: String
let thumbnailURL: URL?
}

我通常有一个像这样的服务层:

protocol DataProvider {
func publisher(forURL url: URL) -> AnyPublisher<Data, Error>
}
struct DefaultDataProvider: DataProvider {
func publisher(forURL url: URL) -> AnyPublisher<Data, Error> {
URLSession.shared.dataTaskPublisher(for: url)
.map(.data)
.mapError { $0 as Error }
.eraseToAnyPublisher()
}
}
struct MockDataProvider: DataProvider {
let data: Data
func publisher(forURL url: URL) -> AnyPublisher<Data, Error> {
return Result.Publisher(.success(data))
.eraseToAnyPublisher()
}
}

MockDataProvider允许我在不进行网络呼叫的情况下注入虚拟数据进行测试。

然后我将有一个服务层提供服务给我的viewControllers。在这种情况下,我可以获取符合Decodable的任何类型的对象,我也可以通过缓存机制获取图像。
struct DecodingService<Output: Decodable> {
let dataProvider: DataProvider
func publisher(forURL url: URL) -> AnyPublisher<Output, Error> {
dataProvider
.publisher(forURL: url)
.decode(type: Output.self, decoder: JSONDecoder())
.eraseToAnyPublisher()
}
}
struct CachedImageService {
let dataProvider: DataProvider
let cache = NSCache<NSString, UIImage>()
func publisher(forURL url: URL) -> AnyPublisher<UIImage, Error> {
let key = url.absoluteString as NSString
if let image = cache.object(forKey: key) {
print("cached")
return Result.Publisher(.success(image))
.eraseToAnyPublisher()
}
print("from web")
return dataProvider
.publisher(forURL: url)
.compactMap(UIImage.init(data:))
.handleEvents(receiveOutput: { image in
self.cache.setObject(image, forKey: key)
})
.eraseToAnyPublisher()
}
}

单元格应该保持较小,并且应该由视图控制器配置或填充:

class DemoCell: UITableViewCell {
static let id = "(DemoCell.self)"
@IBOutlet weak var thumbImageView: UIImageView!
@IBOutlet weak var nameLabel: UILabel!
}

在MVC中,视图控制器应该始终是进行网络调用的那个。

var thumbService: CachedImageService?
var subscriptions = Set<AnyCancellable>()
var models: [DemoModel] = []
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(
withIdentifier: DemoCell.id, for: indexPath) as! DemoCell
let model = models[indexPath.row]
cell.thumbImageView.image = UIImage(named: "placeholder")
cell.nameLabel.text = model.name
guard let url = model.thumbnailURL else { return cell }
thumbService?
.publisher(forURL: url)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: { _ in
}, receiveValue: { thumb in
guard indexPath == tableView.indexPath(for: cell) else { return }
cell.thumbImageView.image = thumb
})
.store(in: &subscriptions)
return cell
}

正如你在这里看到的,视图控制器发出一个网络调用,然后配置单元格。

我建议在你的网络层使用Combine,因为它比使用URLSession本身的回调等要干净得多。