【问题标题】:Passing a model in View (Swift)在视图中传递模型(Swift)
【发布时间】:2021-12-01 14:17:00
【问题描述】:

我正在创建一个基本的 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  
    }
}

【问题讨论】:

  • 以上代码违反了Solid原则。 S -> 单一职责 最好为 URL 调用创建一个单独的 HttpUtility。最好在 cellForRowAt 索引路径中使用“模型”而不是 DataViewCell。
  • 视图,包括 UITableViewCell,应该不知道网络逻辑或任何底层模型类型。如果您要坚持使用 MVC,则只有您的视图控制器应该进行网络调用。它负责模型、网络和视图之间的交互。另外,我建议重命名您的模型。模型只保存数据。更好的名称是 DataProvider 或 DataService。

标签: ios swift model-view-controller uikit


【解决方案1】:

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

首先,您有一个名为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 允许我在不进行网络调用的情况下注入虚拟数据进行测试。

然后我会有一个为我的 viewController 提供服务的服务层。在这种情况下,我可以获取符合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 更干净。

【讨论】:

  • 嗨,罗伯!谢谢你的详细回答。老实说,我现在不了解您的所有代码,但我正在慢慢消化它。没用过Combine,以后会努力学习的!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多