Async image loading with ReactiveCocoa (4.2.1) and Swift - ios

I'm a beginner using ReactiveCocoa with Swift for the first time. I'm building an app showing a list of movies and I'm using the MVVM pattern. My ViewModel looks like this:
class HomeViewModel {
let title:MutableProperty<String> = MutableProperty("")
let description:MutableProperty<String> = MutableProperty("")
var image:MutableProperty<UIImage?> = MutableProperty(nil)
private var movie:Movie
init (withMovie movie:Movie) {
self.movie = movie
title.value = movie.headline
description.value = movie.description
Alamofire.request(.GET, movie.pictureURL)
.responseImage { response in
if let image = response.result.value {
print("image downloaded: \(image)")
self.image.value = image
}
}
}
}
and I would like to configure my cells in the UITableView like this:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("MovieCell", forIndexPath: indexPath) as! MovieCell
let movie:Movie = movieList[indexPath.row]
let vm = HomeViewModel(withMovie: movie)
// fill cell with data
vm.title.producer.startWithNext { (newValue) in
cell.titleLabel.text = newValue
}
vm.description.producer.startWithNext { (newValue) in
cell.descriptioLabel.text = newValue
}
vm.image.producer.startWithNext { (newValue) in
if let newValue = newValue {
cell.imageView?.image = newValue as UIImage
}
}
return cell
}
Is this the right approach for Reactive Cocoa? Do I need to declare Title and description as Mutable or just image (being the only one changing). I think I could use binding but I'm not sure how to proceed.

to do this using Reactive Cocoa + MVVM patterns i would first move all the logic to configure the cell from its viewmodel into the cell class itself. and then remove the MutableProperties from the viewModel (they aren't actually mutable and we dont need those signals). and for the image expose a signal producer that will perform the network request to fetch the image when start() is called, rather than implicitly fetching it when init is called on the ViewModel, giving us something like
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("MovieCell", forIndexPath: indexPath) as! MovieCell
cell.viewModel = self.viewModelForIndexPath(indexPath)
return cell
}
private func viewModelForIndexPath(indexPath: NSIndexPath) -> MovieCellViewModel {
let movie: Movie = movieList[indexPath.row]
return HomeViewModel(movie: movie)
}
and then
class MovieCell: UITableViewCell
#IBOutlet weak var titleLabel: UILabel
#IBOutlet weak var descriptionLabel: UILabel
#IBOutlet weak var imageView: UIImageView
var viewModel: MovieCellViewModel {
didSet {
self.configureFromViewModel()
}
}
private func configureFromViewModel() {
self.titleLabel.text = viewModel.title
self.descriptionLabel.text = viewModel.description
viewModel.fetchImageSignal()
.takeUntil(self.prepareForReuseSignal()) //stop fetching if cell gets reused
.startWithNext { [weak self] image in
self?.imageView.image = image
}
}
//this could also go in a UITableViewCell extension if you want to use it other places
private func prepareForReuseSignal() -> Signal<(), NoError> {
return Signal { observer in
self.rac_prepareForReuseSignal // reactivecocoa builtin function
.toSignalProducer() // obj-c RACSignal -> swift SignalProducer
.map { _ in () } // AnyObject? -> Void
.flatMapError { _ in .empty } // NSError -> NoError
.start(observer)
}
}
}
and in the ViewModel
struct HomeViewModel {
private var movie: Movie
var title: String {
return movie.headline
}
var description: String {
return movie.description
}
func fetchImageSignal() -> SignalProducer<UIImage, NSError> {
return SignalProducer { observer, disposable in
Alamofire.request(.GET, movie.pictureURL)
.responseImage { response in
if let image = response.result.value {
print("image downloaded: \(image)")
observer.sendNext(image) //send the fetched image on the signal
observer.sendCompleted()
} else {
observer.sendFailed( NSError(domain: "", code: 0, userInfo: .None)) //send your error
}
}
}
}

Related

Swift. Doesn't show data on my screen. iOS and Firebase

I faced such problem. When I launch the ios application, I get a white screen and the data that I take from Firebase is not displayed. How can i fix this problem? I would be grateful for your favorite recommendations for solving my problem
This is my ViewController
class ViewController: UIViewController {
#IBOutlet weak var cv: UICollectionView!
var channel = [Channel]()
override func viewDidLoad() {
super.viewDidLoad()
self.cv.delegate = self
self.cv.dataSource = self
let db = Firestore.firestore()
db.collection("content").getDocuments() {( quarySnapshot, err) in
if let err = err {
print("error")
} else {
for document in quarySnapshot!.documents {
if let name = document.data()["title"] as? Channel {
self.channel.append(name)
}
if let subtitle = document.data()["subtitle"] as? Channel {
self.channel.append(subtitle)
}
}
self.cv.reloadData()
}
}
}
}
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return channel.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! ContentCell
let channel = channel[indexPath.row]
cell.setup(channel: channel)
return cell
}
}
This is my Model
struct Content {
let contents: [Channel]
}
struct Channel {
let title: String
let subtitle: String
}
This is my Cell
class ContentCell: UICollectionViewCell {
#IBOutlet weak var channelText: UILabel!
#IBOutlet weak var subtitle: UITextView!
func setup(channel: Channel) {
channelText.text = channel.title
subtitle.text = channel.subtitle
}
}
The data retrieved from Firestore can't just magically be cast to your custom type (Channel); it's a simple dictionary. You eighter need to use Codable or do it manually like so:
I can't tell how exactly to convert it as you have not shared the structure of your data in Firestore, but I assume this will work:
db.collection("content").getDocuments() { (snapshot, error) in
if let error = error {
print("error: \(error.localizedDescription)")
} else if let snapshot = snapshot {
for document in snapshot.documents {
let data = document.data()
if let title = data["title"] as? String,
let subtitle = data["subtitle"] as? String {
self.channel.append(Channel(title: title, subtitle: subtitle))
}
}
}
self.cv.reloadData()
}

How to send Json Data to Table View Array? Swift

I've been researching and wrecking my brain attempting to get my JSON data to load into my tableview. I've tried placing the data in a Variable & I'm able to see the data in the console when I print it, however unable to push it to my table view.
Am I doing something wrong on the data page or am I not properly accessing the data within the loop?
I've tried putting the loop in the viewdidload but haven't been successful either.
// ViewController
import Foundation
import UIKit
import SDWebImage
class EntertainmentViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var A = EntertainmentApi()
var data = [EntertainmentPageData]()
var AA = EntertainmentApi().userFeedPosts
#IBOutlet weak var entPostTableView: UITableView!
override func viewDidLoad() {
func showTable() {
}
entPostTableView.register(EntertainmentViewrTableViewCell.nib(), forCellReuseIdentifier: EntertainmentViewrTableViewCell.identifier)
entPostTableView.delegate = self
entPostTableView.dataSource = self
super.viewDidLoad()
DispatchQueue.main.async {
self.entPostTableView.reloadData()
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let customCell1 = tableView.dequeueReusableCell(withIdentifier: EntertainmentViewrTableViewCell.identifier, for: indexPath) as! EntertainmentViewrTableViewCell
customCell1.profileDisplayName.text = AA[indexPath.row].postDisplayName
self.AA.forEach({ (EntertainmentPageData) in
customCell1.configue(with: EntertainmentPageData.postDisplayName, PostImage: EntertainmentPageData.imageURLString, PostDescription: EntertainmentPageData.postDescription)
})
return customCell1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 2
}
func item(for index: Int) -> EntertainmentPageData {
return data[index]
}
func numberOfItems() -> Int {
return data.count
}
}
//Data
import SwiftUI
import SDWebImage
public protocol EntertainmentPagePostItem {
/// The image for the card.
var imageURLString: String { get }
/// Rating from 0 to 5. If set to nil, rating view will not be displayed for the card.
var postDescription: String? { get }
/// Will be displayed in the title view below the card.
var postDisplayName: String { get }
}
public protocol EntertainmentPagePostDataSource: class {
/// CardSliderItem for the card at given index, counting from the top.
func item(for index: Int) -> EntertainmentPagePostItem
/// Total number of cards.
func numberOfItems() -> Int
}
struct HomePagePost: Codable {
var displayName: String
var cityStatus: String
var displayDescription: String
var displayImageURL: String
var lookingFor: String
var profileImager1: String?
var profileImager2: String?
var profileImager3: String?
var profileImager4: String?
}
struct EntertainmentPageData: Codable {
let postDisplayName: String
let imageURLString: String
let postDescription: String?
}
public class entPostFly: Codable {
let postDisplayName, imageURLString, postDescription: String
}
struct eItem: EntertainmentPagePostItem {
var postDisplayName: String
var imageURLString: String
var postDescription: String?
}
public class EntertainmentApi {
var userFeedPosts = [EntertainmentPageData]()
init() {
load()
}
func load() {
guard let apiURL = URL(string: "https://api.quickques.com/....") else {
return
}
let task: () = URLSession.shared.dataTask(with: apiURL) { Data, apiResponse, error in
guard let Data = Data else { return }
do {
let entPostData = try JSONDecoder().decode([EntertainmentPageData].self, from: Data)
self.userFeedPosts = entPostData
}
catch {
let error = error
print(error.localizedDescription)
}
}.resume()
}
func getFeedPosts(completion: #escaping ([EntertainmentPageData]) -> () ) {
guard let apiURL = URL(string: "https://api.quickques.com/....") else {
return
}
let task: () = URLSession.shared.dataTask(with: apiURL) { Data, apiResponse, error in
guard let Data = Data else { return }
do {
let entPostData = try JSONDecoder().decode([EntertainmentPageData].self, from: Data)
completion(entPostData)
}
catch {
let error = error
print(error.localizedDescription)
}
}.resume()
}
}
class Api {
func getHomePagePosts(completion: #escaping ([HomePagePost]) -> Void ) {
guard let apiURL = URL(string: "https://api.quickques.com/.....") else {
return
}
let task: () = URLSession.shared.dataTask(with: apiURL) { Data, apiResponse, error in
guard let Data = Data else { return }
do {
let homePostData = try JSONDecoder().decode([HomePagePost].self, from: Data)
completion(homePostData)
}
catch {
let error = error
print(error.localizedDescription)
}
}.resume()
}
func getImageData(from url: URL, completion: #escaping (Data?, URLResponse?, Error?) -> ()) {
URLSession.shared.dataTask(with: url, completionHandler: completion).resume()
}
}
func getTopMostViewController() -> UIViewController? {
var topMostViewController = UIApplication.shared.keyWindow?.rootViewController
while let presentedViewController = topMostViewController?.presentedViewController {
topMostViewController = presentedViewController
}
return topMostViewController
}
First you have an empty function showTable inside your viewDidLoad - This does nothing. Presumably it is something hanging around from your various attempts. Delete that.
As you have probably worked out, your network fetch operation is going to occur asynchronously and you need to reload the table view once the data has been fetched.
You have some code in viewDidLoad that kind of tries to do this, but it isn't related to the fetch operation. It is just dispatched asynchronously on the next run loop cycle; This is probably still before the data has been fetched.
However, even if the data has been fetched, it won't show up because you are assigning userFeedPosts from a second instance of your API object to AA at initialisation time. This array is empty and will remain empty since Swift arrays are value types, not reference types. When userFeedPosts is updated, AA will hold the original empty array.
To load the data you need to
Start a load operation when the view loads
Pass a completion handler to that load operation to be invoked when the load is complete
Reload your table view with the new data
class EntertainmentViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var data = [EntertainmentPageData]()
#IBOutlet weak var entPostTableView: UITableView!
override func viewDidLoad() {
entPostTableView.register(EntertainmentViewrTableViewCell.nib(), forCellReuseIdentifier: EntertainmentViewrTableViewCell.identifier)
entPostTableView.delegate = self
entPostTableView.dataSource = self
super.viewDidLoad()
EntertainmentAPI.getFeedPosts { result in
DispatchQueue.main.async { // Ensure UI updates on main queue
switch result {
case .error(let error):
print("There was an error: \(error)")
case .success(let data):
self.data = data
self.entPostTableView.reloadData
}
}
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let customCell1 = tableView.dequeueReusableCell(withIdentifier: EntertainmentViewrTableViewCell.identifier, for: indexPath) as! EntertainmentViewrTableViewCell
let post = data[indexPath.row)
customCell1.profileDisplayName.text = data[indexPath.row].postDisplayName
customCell1.configure(with: post.postDisplayName, PostImage: post.imageURLString, PostDescription: post.postDescription)
return customCell1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
}
public class EntertainmentAPI {
static func getFeedPosts(completion: #escaping ((Result<[EntertainmentPageData],Error>) -> Void) ) {
guard let apiURL = URL(string: "https://api.quickques.com/....") else {
return
}
let task = URLSession.shared.dataTask(with: apiURL) { data, apiResponse, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
/// TODO - Invoke the completion handler with a .failure case
return
}
do {
let entPostData = try JSONDecoder().decode([EntertainmentPageData].self, from: Data)
completion(.success(entPostData))
}
catch {
completion(.failure(error))
}
}.resume()
}
}

Only one switch is on

I'm struggle with following challenge. I created table view with custom cell that contains switch. I wanna only one switch can be on i.e, for instance after launch I switched on 3rd switched and then I switched on 7th switch and thus the 3rd one is switched off and so on. I use rx + protocols for cell and don't understand all the way how to determine which switch was toggled. Previously I was going to use filter or map to look up in dataSource array which switch is on and somehow handle this, but now I messed up with it. I'm not sure it's possible without using table view delegate methods. Thanks a lot, hope someone could explain where I am wrong.
//My cell looks like this:
// CellViewModel implementation
import Foundation
import RxSwift
protocol ViewModelProtocol {
var bag:DisposeBag {get set}
func dispose()
}
class ViewModel:ViewModelProtocol {
var bag = DisposeBag()
func dispose() {
self.bag = DisposeBag()
}
}
protocol CellViewModelProtocol:ViewModelProtocol {
var isSwitchOn:BehaviorSubject<Bool> {get set}
}
class CellVM:ViewModel, CellViewModelProtocol {
var isSwitchOn: BehaviorSubject<BooleanLiteralType> = BehaviorSubject(value: false)
let internalBag = DisposeBag()
override init() {
}
}
//My Cell implementation
import UIKit
import RxSwift
import RxCocoa
class Cell:UITableViewCell {
static let identifier = "cell"
#IBOutlet weak var stateSwitch:UISwitch!
var vm:CellViewModelProtocol? {
didSet {
oldValue?.dispose()
self.bindUI()
}
}
var currentTag:Int?
var bag = DisposeBag()
override func awakeFromNib() {
super.awakeFromNib()
self.bindUI()
}
override func prepareForReuse() {
super.prepareForReuse()
self.bag = DisposeBag()
}
private func bindUI() {
guard let vm = self.vm else { return }
self.stateSwitch.rx.controlEvent(.valueChanged).withLatestFrom(self.stateSwitch.rx.value).observeOn(MainScheduler.asyncInstance).bind(to: vm.isSwitchOn).disposed(by: vm.bag)
}
}
//TableViewController implementation
import UIKit
import RxSwift
import RxCocoa
class TableViewController: UITableViewController {
private var dataSource:[CellViewModelProtocol] = []
var vm = TableViewControllerVM()
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.estimatedRowHeight = 70
self.tableView.rowHeight = UITableView.automaticDimension
self.bindUI()
}
private func bindUI() {
vm.dataSource.observeOn(MainScheduler.asyncInstance).bind { [weak self] (dataSource) in
self?.dataSource = dataSource
self?.tableView.reloadData()
}.disposed(by: vm.bag)
}
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.dataSource.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: Cell.identifier, for: indexPath) as! Cell
if cell.vm == nil {
cell.vm = CellVM()
}
return cell
}
}
class TableViewControllerVM:ViewModel {
var dataSource:BehaviorSubject<[CellViewModelProtocol]> = BehaviorSubject(value: [])
let internalBag = DisposeBag()
override init() {
super.init()
dataSource.onNext(createDataSourceOf(size: 7))
self.handleState()
}
private func createDataSourceOf(size:Int) -> [CellViewModelProtocol] {
var arr:[CellViewModelProtocol] = []
for _ in 0..<size {
let cell = CellVM()
arr.append(cell)
}
return arr
}
private func handleState() {
}
}
Maybe this code will help you:
extension TableViewController {
// called from viewDidLoad
func bind() {
let cells = (0..<7).map { _ in UUID() } // each cell needs an ID
let active = ReplaySubject<UUID>.create(bufferSize: 1) // tracks which is the currently active cell by ID
Observable.just(cells) // wrap the array in an Observable
.bind(to: tableView.rx.items(cellIdentifier: "Cell", cellType: Cell.self)) { _, element, cell in
// this subscription causes the inactive cells to turn off
active
.map { $0 == element }
.bind(to: cell.toggleSwitch.rx.isOn)
.disposed(by: cell.disposeBag)
// this subscription watches for when a cell is set to on.
cell.toggleSwitch.rx.isOn
.filter { $0 }
.map { _ in element }
.bind(to: active)
.disposed(by: cell.disposeBag)
}
.disposed(by: disposeBag)
}
}
Have a similar UI,so tested locally and it works.But not very neat code.
ProfileCellViewModel
struct ProfileCellViewModel {
// IMPORTANT!!!
var bibindRelay: BehaviorRelay<Bool>?
}
ProfileCell
final class ProfileCell: TableViewCell {
#IBOutlet weak var topLabel: Label!
#IBOutlet weak var centerLabel: Label!
#IBOutlet weak var bottomLabel: Label!
#IBOutlet weak var onSwitch: Switch!
public var vm: ProfileCellViewModel? {
didSet {
// IMPORTANT!!!
if let behaviorRelay = vm?.bibindRelay {
(onSwitch.rx.controlProperty(editingEvents: .valueChanged,
getter: { $0.isOn }) { $0.isOn = $1 } <-> behaviorRelay)
.disposed(by: self.rx.reuseBag)
}
}
}
}
ProfileViewModel
final class ProfileViewModel: ViewModel, ViewModelType {
struct Input {
let loadUserProfileStarted: BehaviorRelay<Void>
}
struct Output {
let userItems: BehaviorRelay<[ProfileCellViewModel]>
let chatRelay: BehaviorRelay<Bool>
let callRelay: BehaviorRelay<Bool>
}
let input = Input(loadUserProfileStarted: BehaviorRelay<Void>(value: ()))
let output = Output(userItems: BehaviorRelay<[ProfileCellViewModel]>(value: []),
chatRelay: BehaviorRelay<Bool>(value: false),
callRelay: BehaviorRelay<Bool>(value:false))
override init() {
super.init()
// IMPORTANT!!!
Observable.combineLatest(output.chatRelay,output.callRelay).pairwise().map { (arg0) -> Int in
let (pre, curr) = arg0
let preFlag = [pre.0,pre.1].filter { $0 == true }.count == 1
let currFlag = [curr.0,curr.1].filter { $0 == true }.count == 2
if preFlag && currFlag {
return [pre.0,pre.1].firstIndex(of: true) ?? 0
}
return -1
}.filter {$0 >= 0}.subscribe(onNext: { (value) in
[self.output.chatRelay,self.output.callRelay][value].accept(false)
}).disposed(by: disposeBag)
}
private func createProfileCellItems(user: User) -> [ProfileCellViewModel] {
// IMPORTANT!!!
let chatCellViewModel = ProfileCellViewModel(topText: nil,
centerText: R.string.i18n.chat(),
bottomText: nil,
switchStatus: true,
bibindRelay: output.chatRelay)
// IMPORTANT!!!
let callCellViewModel = ProfileCellViewModel(topText: nil,
centerText: R.string.i18n.call(),
bottomText: nil,
switchStatus: true,
bibindRelay: output.callRelay)
return [roleCellViewModel,
teamCellViewModel,
statusCellViewModel,
sinceCellViewModel,
chatCellViewModel,
callCellViewModel]
}
}
I mark the codes you should pay attention to with // IMPORTANT!!!

Show file uploading progress on tableview cell Swift 4

I am picking files from iCloud and uploading into AWS S3. I am listing selected files with progress bar uploading status in tableview cell. Each cell separate file title and loading progress I am maintaining. Here, everything almost done but If I upload two files tableview first cell got freezed and second cell doing upload progress.
My upload function
private func upload(file url: URL, keyname : String, exten: String) {
let bucket = S3BucketName
let key = keyname
let contentType = "text/\(exten)"
let expression = AWSS3TransferUtilityUploadExpression()
expression.progressBlock = progressBlock
let task = transferUtility.uploadFile(url,
bucket: bucket,
key: key,
contentType: contentType,
expression: expression,
completionHandler: completionHandler)
task.continueWith { (task) -> Any? in
if let error = task.error {
DispatchQueue.main.async {
//self.infoLabel.text = "Error: \(error.localizedDescription)"
}
return nil
}
if let uploadTask = task.result {
self.uploadTask = uploadTask
DispatchQueue.main.async {
//self.infoLabel.text = "Generating Upload File"
//self.uploadRequests.append(self.uploadTask)
self.tableView_util.reloadData()
}
}
return nil
}
}
Tableview cell
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cellutil", for: indexPath) as! UtilityTableViewCell
let item = tableArray[indexPath.row]
cell.name_label_util.text = item.title
cell.control_button_util.tag = indexPath.row
cell.control_button_util.addTarget(self, action: #selector(playpause), for: .touchUpInside)
// MARK - Upload process
progressBlock = { [weak self] task, progress in
guard let strongSelf = self else { return }
DispatchQueue.main.async {
cell.loader_Line_util.progress = Float(progress.fractionCompleted)
//NSLog(#"fraction completed: %f", progress.fractionCompleted);
let percentageUploaded:Float = Float(progress.fractionCompleted) * 100
cell.statusLabel_util.text! = NSString(format:"Uploading: %.0f%%",percentageUploaded) as String
// Need to change
if cell.statusLabel_util.text == "Uploading: 100%" {
cell.statusLabel_util.text = "File Successfully Uploaded!"
cell.loader_Line_util.progress = 1;
self?.tableView_util.reloadData()
}
}
}
completionHandler = { [weak self] task, error in
guard let strongSelf = self else { return }
if let error = error {
DispatchQueue.main.async {
}
return
}
Ok, I had to look at AWS TransferUtility, never used it before. I think you'll need to refactor some stuff. I wrote this in notepad so likely so syntax errors and I left some functions empty, since you can add what goes there.
//create a class to track all your uploads in progress
Class UploadTaskTracker {
let shared = uploadTaskTracker()
private var tasks = [UploadTask]()
func addTask( _ task: UploadTask){
tasks.append(task)
}
fund updateTask( id: String, progress: Double){
// get task from array and update progress value
}
func completeTask(id: String) {
// remove task from tasks array
// send out a notification to trigger tableview to reload data
}
func activeTasks() -> Int {
return tasks.count
}
func taskAt(_ indexPath: IndexPath) -> UploadTask{
// check for out of bounds
// return task
}
}
Class UploadTask {
var id : String
var progress : Double
}
// and when you start an upload task, change your expression progress block
// (and remove it from the cell. This should all be happening in another
// class and not be associated with your tableview
let guid = UUID()
let uploadTask = UploadTask.init(id: guid, progress: 0.0)
UploadTaskTracker.shared.addTask(uploadTask_
expression.progressBlock = {(task, progress) in
UploadTaskTracker.shared.updateTask(id: guid, progress: progress)
}
// then in table view controller
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return UploadTaskTracker.shared.activeTasks()
}
//And then in
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
// dequeue custom uitableviewcel
let uploadTask = UploadTaskTracker.shared. taskAt( indexPath)
cell.load(uploadTask)
}
// create a custom uitableviewcell class
class customCell: UITableViewCell {
#IBOutlet weak var someLabel: UILabel!
var observers = [NSKeyValueObservation]()
override func prepareForReuse() {
// clear previous UI to reduce chance of user seeing obsolete text or image
super.prepareForReuse()
stopObservers()
}
func stopObservers(){
for observer in observers{
observer.invalidate()
}
observers.removeAll()
}
func load(_ uploadTask: UploadTask ) {
let observer = uploadTask.observe(\.progress, options: [.initial, .new]) { [weak self] (uploadTask, change) in
DispatchQueue.main.async {
someLabel.text = “\(uploadTask.progress)”
}
}
observers.append(observer)
}

Images loaded from URL changing on scroll in UITableView

I currently have a tableView being loaded from Firebase. This content includes a picture which, when the user scrolls, will change until it settles on its final image. I would assume this would be assigning an image to each cell before it can succesfully load each cell, but have not been able to come up with a work around.
The current code that I have to populate the tableView is as follows:
TableView
import UIKit
import FirebaseAuth
import FirebaseStorage
class Articles: UITableViewController {
var vcType:String = "Home"
var rooms = [Room]()
var articleCell = ArticlesCell()
#IBOutlet weak var menuButton: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
if self.vcType == "Home"
{
self.rooms += ArticlesManager.sharedClient.rooms
}
else
{
if let obj = ArticlesManager.sharedClient.catRooms[self.vcType.lowercased()] //as? [Room]
{
self.rooms += obj
}
}
self.tableView.reloadData()
ArticlesManager.sharedClient.blockValueChangeInRoomArray = {
newRoom in
if self.vcType == "Home"
{
self.rooms.append(newRoom)
self.rooms.sort(by: {
if $0.created_Date == nil
{
return false
}
if $1.created_Date == nil
{
return true
}
return $0.created_Date.compare($1.created_Date) == ComparisonResult.orderedDescending
})
}
else
{
if self.vcType.lowercased() == newRoom.category
{
self.rooms.append(newRoom)
self.rooms.sort(by: {
if $0.created_Date == nil
{
return false
}
if $1.created_Date == nil
{
return true
}
return $0.created_Date.compare($1.created_Date) == ComparisonResult.orderedDescending
})
self.tableView.reloadData()
}
}
}
}
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return rooms.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if (indexPath as NSIndexPath).row == 0 {
let cell2 = tableView.dequeueReusableCell(withIdentifier: "featured", for: indexPath) as! featuredCell
let room = rooms[(indexPath as NSIndexPath).row]
cell2.configureCell(room)
return cell2
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! ArticlesCell
let room = rooms[(indexPath as NSIndexPath).row]
cell.configureCell(room)
return cell
}
Data
import Foundation
import Firebase
import FirebaseAuth
let roomRef = FIRDatabase.database().reference()
class Data {
static let dataService = Data()
fileprivate var _BASE_REF = roomRef
fileprivate var _ROOM_REF_ = roomRef.child("rooms")
fileprivate var _BASE_REF2 = roomRef
fileprivate var _ROOM_REF_2 = roomRef.child("contents")
var BASE_REF: FIRDatabaseReference {
return _BASE_REF
}
var ROOM_REF: FIRDatabaseReference {
return _ROOM_REF_
}
var storageRef: FIRStorageReference {
return FIRStorage.storage().reference()
}
var fileURL: String!
func fetchData(_ callback: #escaping (Room) -> ()) {
Data.dataService.ROOM_REF.observe(.childAdded, with: { (snapshot) in
print("snapshot.value - \(snapshot))")
let room = Room(key: snapshot.key, snapshot: snapshot.value as! Dictionary<String, AnyObject>)
callback(room)
})
}
}
TableViewCell
import UIKit
import FirebaseStorage
class featuredCell: UITableViewCell {
#IBOutlet weak var featuredImage: UIImageView!
#IBOutlet weak var featuredTitle: UITextView!
#IBOutlet weak var featuredAuthor: UITextField!
#IBOutlet weak var featuredDate: UITextField!
#IBOutlet weak var featuredContent: UITextView!
class var defaultHeight: CGFloat { get { return ((UIScreen.main.fixedCoordinateSpace.bounds.height - 64) / 5) * 3}}
func configureCell(_ room: Room) {
self.featuredTitle.text = room.title
self.featuredDate.text = room.date
self.featuredAuthor.text = room.author
self.featuredContent.text = room.story
if let imageURL = room.thumbnail {
if imageURL.hasPrefix("gs://") {
FIRStorage.storage().reference(forURL: imageURL).data(withMaxSize: INT64_MAX, completion: { (data, error) in
if let error = error {
print("Error downloading: \(error)")
return
}
self.featuredImage.image = UIImage.init(data: data!)
})
} else if let url = URL(string: imageURL), let data = try? Foundation.Data(contentsOf: url) {
self.featuredImage.image = UIImage.init(data: data)
}
}
}
}
Thank you for your help!
This is a normal behavior of tableviews when working with async downloading during tableview load or scrolling. You've to just clear your image before loading and keep the images datas in Cache when they've downloaded once for a efficient network charge. If you do not use caching, tableview always "re-download" the image when the cell is going to reused. Please see the example;
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "cell") as! TableViewCell
let urle = self.filteredFlats[indexPath.row].flatThumbnailImage?.imageDownloadURL
if let url = urle
{
cell.imgv.image = nil
if let imagefromCache = imageCache.object(forKey: url as AnyObject) as? UIImage
{
cell.imgv.image = imagefromCache
}
else
{
cell.imgv.image = nil
let urlURL = URL(string: (url))
URLSession.shared.dataTask(with: urlURL!, completionHandler: { (data, response, error) in
if error != nil
{
print(error.debugDescription)
return
}
DispatchQueue.main.async {
let imagetoCache = UIImage(data:data!)
self.imageCache.setObject(imagetoCache!, forKey: url as AnyObject)
cell.imgv.image = imagetoCache
}
}).resume()
}
}
return cell
}
this code is copied from my project. So i'm caching the image when it is already downloaded. Also i do not redownload it when cell is reused. Just checking if it is cached or not. So you can use this apporach for a behaviour that satisfy your requirements.

Resources