I have a UI widget as follows
class ProfileBubbleCell: UITableViewCell {
var roundImageView: UIImageView?
override func awakeFromNib() {
super.awakeFromNib()
self.contentView.backgroundColor = Color.red
initImage()
}
private func initImage(){
let imageView = UIImageView()
let width = self.frame.width
let height = self.frame.height
let img_width = height - 4
let img_height = img_width
let y = 2
let x = width/2 - img_width/2
imageView.frame = CGRect(
x: x, y: CGFloat(y), width: img_width, height: img_height
)
let rounded = imageView
.makeRounded()
.border(width:2.0, color:Color.white.cgColor)
// attach and save reference
self.addSubview(rounded)
self.roundImageView = rounded
}
private func loadImage(){
// #TODO: call parent function
}
}
And in loadImage, I would like to call the parent's image loading view, and when the image is loaded, display it in roundImageView. ProfileBubbleCell is really meant to be as generic as possible, its only concern is making the image round and centering it.
This looks like a very common use case and I would like to delegate the loading image task to the parent, but I am not sure how to express it.
In the parent I instantiate the cell as follows:
let cell = tableView.dequeueReusableCell(withIdentifier: "ProfileBubbleCell", for: indexPath) as! ProfileBubbleCell
Here show you some about delegate use.
// 1) define delegate.
protocol ProfileBubbleCellDelegate {
func getImage() -> UIImage?
}
class ProfileBubbleCell: UITableViewCell {
// 2) declare a variable of ProfileBubbleCellDelegate
weak var delegate: ProfileBubbleCellDelegate?
//
func configure() {
self.roundImageView.image = delegate.getImage()
}
}
// when dequeueReuseCell on cellForRow(at:)
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "ProfileBubbleCell", for: indexPath) as ProfileBubbleCell else { return UITableView() }
// 3) assign delegate to tableView's superView, maybe it's a UIViewController or UIView on your class.
cell.delegate = self
cell.configure()
return cell
}
// 4) extension Your class who be assigned delegate of ProfileBubbleCellDelegate
// and implement delegate's method.
extension YourClass: ProfileBubbleCellDelegate {
func getImage() -> UIImage? {
// 5) provide image.
return hereProvideYourImage
}
}
// or if you want immediately download image when cell.roundedImageView need it.
// modify the getImage() of Delegate like this.
protocol ProfileBubbleCellDelegate {
func getImage(completion: #escaping ((UIImage?) -> Void))
}
// then the extension implement will be
extension YourClass: ProfileBubbleCellDelegate {
func getImage(completion: #escaping ((UIImage?) -> Void)) {
downloadImageTask.downloadImage(url: imageUrl, completion: { downloadedImage in
// because completion will be passed on other closure of downloadImage(completion:),
// so delegate method need add `#escaping` that means the completion can escape from itself closure.
completion?(downloadedImage)
})
}
}
// don't forget replace called method on cell.
class ProfileBubbleCell: UITableViewCell {
// ...
func configure() {
delegate.getImage(completion: { downloadedImage in
DispatchQueue.main.async {
self.roundImageView.image = downloadedImage
}
})
}
}
Related
I want to use a Combine in my project and face the problem.
Here is the code of the ViewController
import Combine
import UIKit
class ProfileDetailsController: ViewController {
//
// MARK: - Views
#IBOutlet private var tableView: UITableView!
// MARK: - Properties
private typealias DataSource = UITableViewDiffableDataSource<ProfileDetailsSection, ProfileDetailsRow>
private typealias Snapshot = NSDiffableDataSourceSnapshot<ProfileDetailsSection, ProfileDetailsRow>
#Published private var data: [ProfileDetailsSectionModel] = {
return ProfileDetailsSection.allCases.map { ProfileDetailsSectionModel(section: $0, data: $0.rows) }
}()
private lazy var dataSource: DataSource = {
let dataSource = DataSource(tableView: tableView) { tableView, _, model in
let cell = tableView.dequeueReusableCell(withIdentifier: TextFieldTableCell.name) as! TextFieldTableCell
cell.delegate = self
cell.setData(model: model)
return cell
}
dataSource.defaultRowAnimation = .fade
return dataSource
}()
}
// MARK: - Setup binding
extension ProfileDetailsController {
override func setupBinding() {
tableView.registerCellXib(cell: TextFieldTableCell.self)
$data.receive(on: RunLoop.main).sink { [weak self] models in
let sections = models.map { $0.section }
var snapshot = Snapshot()
snapshot.appendSections(sections)
models.forEach { snapshot.appendItems($0.data, toSection: $0.section) }
self?.dataSource.apply(snapshot, animatingDifferences: true)
}.store(in: &cancellable)
}
}
// MARK: - Cell delegates
extension ProfileDetailsController: TextFieldTableCellDelegate {
func switcherAction() { }
}
And here is the code of the cell.
import UIKit
protocol TextFieldTableCellData {
var placeholder: String? { get }
}
protocol TextFieldTableCellDelegate: NSObjectProtocol {
func switcherAction()
}
class TextFieldTableCell: TableViewCell {
//
// MARK: - Views
#IBOutlet private var textField: ZWTextField!
// MARK: - Properties
public weak var delegate: TextFieldTableCellDelegate?
override class var height: CGFloat {
return 72
}
}
// MARK: - Public method
extension TextFieldTableCell {
func setData(model: TextFieldTableCellData) {
textField.placeholder = model.placeholder
}
}
ViewController's deinit was not called.
But when I use this code for ViewController
import UIKit
class ProfileDetailsController: ViewController {
//
// MARK: - Views
#IBOutlet private var tableView: UITableView!
// MARK: - Properties
#Published private var data: [ProfileDetailsSectionModel] = {
return ProfileDetailsSection.allCases.map { ProfileDetailsSectionModel(section: $0, data: $0.rows) }
}()
}
// MARK: - Startup
extension ProfileDetailsController {
override func startup() {
tableView.dataSource = self
tableView.registerCellXib(cell: TextFieldTableCell.self)
}
}
// MARK: - Startup
extension ProfileDetailsController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return data.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data[section].data.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let model = data[indexPath.section].data[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: TextFieldTableCell.name) as! TextFieldTableCell
cell.delegate = self
cell.setData(model: model)
return cell
}
}
// MARK: - Cell delegates
extension ProfileDetailsController: TextFieldTableCellDelegate {
func switcherAction() {}
}
Everything is fine. deinit called. I tried to set dataSource optional and set it nil on deinit, the same result. With Combine deinit called only when I comment this line:
cell.delegate = self
Does anyone know what's the matter?
Xcode 13.2 iOS 15.2
The Combine stuff is a total red herring. That's why you can't locate the problem; you're looking in the wrong place. The issue is the difference between an old-fashioned data source and a diffable data source. The problem is here:
private lazy var dataSource: DataSource = { // *
let dataSource = DataSource(tableView: tableView) { tableView, _, model in
let cell = tableView.dequeueReusableCell(withIdentifier: TextFieldTableCell.name) as! TextFieldTableCell
cell.delegate = self // *
I've starred the problematic lines:
On the one hand, you (self, the view controller) are retaining the dataSource.
On the other hand, you are giving the data source a cell provider function in which you speak of self.
That's a retain cycle! You need to break that cycle. Change
let dataSource = DataSource(tableView: tableView) { tableView, _, model in
To
let dataSource = DataSource(tableView: tableView) { [weak self] tableView, _, model in
(That will compile, because although self is now an Optional, so is cell.delegate.)
I have tableView that uses a NSFetchedResultsController to populate data. When clicking on a cell, it takes you to a detailViewController of that object. And the following two properties are pushed forward with prepare(for:).
var coreDataStack: CoreDataStack!
var selectedGlaze: Glaze?
Inside the detailView, I have 2 cells. The first is cell that contains a scrollView with an array of images:
import UIKit
protocol SwipedRecipeImageViewDelegate: class {
func recipeImageViewSwiped(_ cell: RecipePhotoTableViewCell, selectInt: Int)
}
class RecipePhotoTableViewCell: UITableViewCell, UIScrollViewDelegate {
#IBOutlet weak var scrollView: UIScrollView!
#IBOutlet weak var pageControl: UIPageControl!
// -
var imagesArray: [Data] = []
var selectedImageData: Int = 0
// -
weak var delegate: SwipedRecipeImageViewDelegate?
// -
override func awakeFromNib() {
super.awakeFromNib()
self.backgroundColor = .clear
self.selectionStyle = .none
scrollView.delegate = self
scrollView.isUserInteractionEnabled = false // Allows didSelectAtRow:
contentView.addGestureRecognizer(scrollView.panGestureRecognizer) // Allows Scrolling
}
override func layoutSubviews() {
super.layoutSubviews()
setImages()
setOffsetX(pageNumber: selectedImageData)
}
func configureCell(section: Int, row: Int, images: [RecipeImage], arrayInt: Int, delegate: SwipedRecipeImageViewDelegate) {
self.delegate = delegate
selectedImageData = arrayInt
for image in images {
guard let imageData = image.recipeImageData else { return }
imagesArray.append(imageData)
}
}
#IBAction func pageChanged(_ sender: UIPageControl) {
setOffsetX(pageNumber: sender.currentPage)
}
func setOffsetX(pageNumber: Int) {
pageControl.currentPage = pageNumber
let offsetX = contentView.bounds.width * CGFloat(pageNumber)
DispatchQueue.main.async {
UIView.animate(withDuration: 0.2, delay: 0, options: UIView.AnimationOptions.curveEaseOut, animations: {
self.scrollView.contentOffset.x = offsetX
}, completion: nil)
}
}
func setImages() {
// Set Page Count:
pageControl.numberOfPages = imagesArray.count
// Set Frame For ImageViews + Scroll View:
for index in 0..<imagesArray.count {
let imageView = UIImageView()
imageView.frame.size = contentView.bounds.size
imageView.frame.origin.x = contentView.bounds.width * CGFloat(index)
imageView.image = UIImage(data: imagesArray[index])
imageView.contentMode = .scaleAspectFill
imageView.layer.masksToBounds = true // Limits Frame Size
scrollView.addSubview(imageView)
}
// Set ScrollView Size:
scrollView.contentSize = CGSize(width: (contentView.bounds.width * CGFloat(imagesArray.count)), height: contentView.bounds.height)
scrollView.delegate = self
}
// Set Page Number:
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
let pageNumber = scrollView.contentOffset.x / scrollView.frame.size.width
self.pageControl.currentPage = Int(pageNumber)
delegate?.recipeImageViewSwiped(self, selectInt: pageControl.currentPage)
}
The second cell contains a stackView with some labels to display data that the image shows. It accepts a lot of parameters and then sets the textColor and changes some labels. Nothing too exciting so I didn't include the code.
DetailViewController:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
print("cellForRowAt: ", indexPath)
switch indexPath.section {
case sectionImage: // Section 0:
guard
let images = selectedGlaze?.glazeImage,
let glazeImageSelected = selectedGlaze?.glazeImageSelected // This is a Double
else { return returnDefaultCell() }
let imageArray = images.allObjects as! [RecipeImage] // Takes NSSet of relational data and changes it into an Array to be passed into the image cell.
let imageSelected = Int(glazeImageSelected) // Double Converted to Int
switch indexPath.row {
case 0:
let cell = returnRecipeImageCell()
return configureRecipeImageCell(cell: cell, for: indexPath, imagesArray: imageArray, imageSelected: imageSelected)
case 1:
let cell = returnAtmosphereCell()
return configureAtmosphereCell(cell: cell, for: indexPath, imagesArray: imageArray, imageSelected: imageSelected)
default: return returnDefaultCell()
}
}
}
SwipedRecipeImageViewDelegate:
func recipeImageViewSwiped(_ cell: RecipePhotoTableViewCell, selectInt: Int) {
selectedGlaze?.glazeImageSelected = Double(selectInt)
coreDataStack.saveContext()
DispatchQueue.main.async { //
self.tableView.beginUpdates() //
let row1: IndexPath = [0,1] //
self.tableView.reloadRows(at: [row1], with: .automatic) //
self.tableView.endUpdates() //
}
}
The Issue:
The issue I'm having is reloading the second cell to be updated with the correct information after the recipeImageViewSwiped() is called. Seen here: https://imgur.com/a/fIYfehf
This happens when the code inside the DispatchQueue.main.async block is active. When the block is comment out, this happens: https://imgur.com/a/fYUVZKH - Which is what I'd expect. (Other than the cell at [0,1] isn't updated).
Specifically, when the tableView reloads row [0,1], cellForRowAt() only gets called on that row, [0,1]. But I'm not sure why the cell at [0,0], with the image, flicks back to the original image shown in the scrollView.
Goal:
My goal is to have the cell with the scrollView not flicker after being swiped on. But also to save the context, so that the object can save which image in the array is selected. And then to update/reload the second cell with the new information the image that's selected, so it can update it's labels correctly.
EDIT:
Removing the following in layoutSubviews() has this affect: https://imgur.com/a/vwrZfus - Which looks like it's mostly working. But still has a strange animation.
override func layoutSubviews() {
super.layoutSubviews()
setImages()
// setOffsetX(pageNumber: selectedImageData)
}
EDIT 2:
This looks like its entirely an issue with setting up the cell's view. Along with layout Subviews.
EDIT 3:
I added a Bool: hasSetLayout and a switch inside of layoutSubviews() and it appears to be working as I want. - However if any one still has any information to help me understand this issue, I'd appreciate it.
var hasSetLayout = false
override func layoutSubviews() {
super.layoutSubviews()
switch hasSetLayout {
case false: setImages(selectedPhoto: selectedImageData)
default: break
}
}
try to reload row without animation :
UIView.performWithoutAnimation {
tableView.reloadRows(at: [indexPath], with: .none)
}
I am struggling on how to display a system message in a chatroom. Currently, I have already built the chatroom using MessageKit in Swift and finished the overall UI. What I want is, when I click the red button at the upper right corner, a system message "The button is tapped" will automatically display in the chatroom, like the sample picture below. Thanks a lot if anyone can help!
sample picture
This answer may be a bit late but the solution is quite simple. All you need to do is create a custom cell and configure it with the messages view controller data source.
There's a comprehensive guide you can follow on the official repo for help with what to do alongside an example
Here's some demo code (you won't be able to copy and paste this since I'm referencing some custom UIView subclasses)
Firstly, create your UICollectionViewCell
class SystemMessageCell: CollectionCell<MessageType> {
lazy var messageText = Label()
.variant(.regular(.small))
.color(.gray)
.align(.center)
override func setupView() {
addSubview(messageText)
}
override func setupConstraints() {
messageText.snp.makeConstraints { $0.edges.equalToSuperview() }
}
override func update() {
guard let item = item else { return }
switch item.kind {
case .custom(let kind):
guard let kind = kind as? ChatMessageViewModel.Kind else { return }
switch kind {
case .system(let systemMessage):
messageText.text = systemMessage
}
default:
break
}
}
}
Secondly, you need to create two separate files.
One is a size calculator and the other is a custom flow layout.
class SystemMessageSizeCalculator: MessageSizeCalculator {
override func messageContainerSize(for message: MessageType) -> CGSize {
// Just return a fixed height.
return CGSize(width: UIScreen.main.bounds.width, height: 20)
}
}
class SystemMessageFlowLayout: MessagesCollectionViewFlowLayout {
lazy open var sizeCalculator = SystemMessageSizeCalculator(layout: self)
override open func cellSizeCalculatorForItem(at indexPath: IndexPath) -> CellSizeCalculator {
let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
if case .custom = message.kind {
return sizeCalculator
}
return super.cellSizeCalculatorForItem(at: indexPath);
}
}
Finally, hook it all up in your MessagesViewController like so:
class ChatViewController: MessagesViewController {
override func viewDidLoad() {
messagesCollectionView = MessagesCollectionView(frame: .zero, collectionViewLayout: SystemMessageFlowLayout())
super.viewDidLoad()
messagesCollectionView.register(SystemMessageCell.self)
}
// MARK: - Custom Cell
override open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
fatalError("Ouch. nil data source for messages")
}
let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
if case .custom = message.kind {
let cell = messagesCollectionView.dequeueReusableCell(SystemMessageCell.self, for: indexPath)
cell.item = message
return cell
}
return super.collectionView(collectionView, cellForItemAt: indexPath)
}
}
The end result will be something like this:
I have been unable to use the didHighlightItem and didUnhighlightItem functions to correctly animate when a cell is clicked, as I am unable to access the cell's section number. I was wondering whether I am solving the problem incorrectly, or whether there is a way of accessing the SectionController's section index in the collection view. My code is as follows:
class PersonSectionController: ListSectionController {
var current: Person?
override init() {}
override func didUpdate(to object: Any) {
if let person = object as? Person {
current = person
}
}
// Number of items displayed per object
override func numberOfItems() -> Int {
return 1
}
// Dequeue and configure cell
override func cellForItem(at index: Int) -> UICollectionViewCell {}
// Returns the required cell size at the given index
override func sizeForItem(at index: Int) -> CGSize {}
override func didHighlightItem(at index: Int) {
if let viewController = self.viewController as? PeopleViewController {
if let cell = viewController.peopleCollectionView.cellForItem(at: IndexPath(row: index, section: 0)) {
UIView.animate(withDuration: 0.1) {
cell.transform = CGAffineTransform(scaleX: 0.95, y: 0.95)
}
}
}
}
override func didUnhighlightItem(at index: Int) {
if let viewController = self.viewController as? PeopleViewController {
if let cell = viewController.peopleCollectionView.cellForItem(at: IndexPath(row: 0, section: index)) {
UIView.animate(withDuration: 0.1) {
cell.transform = CGAffineTransform.identity
}
}
}
}
}
Thanks in advance!
I think that you need to access the cell from the collectionContext:
override func didHighlightItem(at index: Int) {
guard let cell = collectionContext?.cellForItem(at: index) else { return }
// Animation stuff
}
I am experiencing very choppy scrolling in my collection view. There are only 10 cells. It is because of my method of retrieving the images which is to take the URL and turn it to UIImage data.
Images variable is just an array of image URLs.
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return images.count
}
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "mediaCells", for: indexPath) as! MediaCell
let url = URL(string: images[indexPath.row] as! String)
let data = try? Data(contentsOf: url!)
cell.mediaImage.image = UIImage(data: data!)
return cell
}
A solution I found was to do this first for all images. The problem with this approach is that it will have to wait for all images to download the data and append to the array before we display the collectionView. This can take quite a few seconds 10-15 seconds before we can render the collection. Too slow!
override func viewDidLoad() {
super.viewDidLoad()
Alamofire.request(URL(string: "myURL")!,
method: .get)
.responseJSON(completionHandler: {(response) -> Void in
if let value = response.result.value{
let json = JSON(value).arrayValue
for item in json{
let url = URL(string: item["image"].string!)
let data = try? Data(contentsOf: url!)
self.images.append(data)
}
self.collectionView.reloadData()
}
})
}
Used SDWebImage from cocoapods to do Async image fetching.
https://cocoapods.org/?q=SDWebImage
cell.mediaImage.sd_setImage(with: URL(string: images[indexPath.row] as! String))
Some will shout about the use of globals, but this is the way I did previously:
Declare a global array of UIImage
var images: [UIImage] = []
When image is downloaded, append it to that array
for item in json{
let url = URL(string: item["image"].string!)
let data = try? Data(contentsOf: url!)
let image = UIImage(data: data!)
images.append(image)
NotificationCenter.default.post(Notification.Name(rawValue: "gotImage"), nil)
}
Follow the Notification in your VC
override function viewDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(collectionView.reloadData()), name: NSNotification.Name(rawValue: "gotImage"), object: nil)
super.viewDidLoad()
}
Do not assign an image if not available in your cell for collectionView
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
.....
if let image = images[indexPath].row {
cell.mediaImage.image = image
}
....
}
PS: If all the downloading and loading action happens in the same VC, just declare images inside your VC class.
Seems like you need to do the lazy loading of images in your collection view based on the visible cells only. I have implemented the same approach like this.
1) First steps you need to create a class named it as PendingOperations.swift
class PendingOperations {
lazy var downloadsInProgress = [IndexPath: Operation]()
lazy var downloadQueue: OperationQueue = {
var queue = OperationQueue()
queue.name = "downloadQueue"
queue.maxConcurrentOperationCount = 1
return queue
}()
}
2) Seconds steps create a class named it as ImageDownloader where you can make your network call to download the image.
class ImageDownloader: Operation {
let imageId: String
init(imageId: String) {
self.imageId = imageId
}
override func main() {
if self.isCancelled {
return
}
Alamofire.request(URL(string: "myURL")!,
method: .get)
.responseJSON(completionHandler: {(response) -> Void in
if let value = response.result.value{
let json = JSON(value).arrayValue
for item in json{
let url = URL(string: item["image"].string!)
self.image = try? Data(contentsOf: url!)
})
if self.isCancelled {
return
}
}
}
3) Third steps implement some methods inside viewController to manipulate the operations based on the visible cells.
func startDownload(_ image: imageId, indexPath: IndexPath) {
if let _ = pendingOperations.downloadsInProgress[indexPath] {
return
}
let downloader = ImageDownloader(image: imageId)
downloader.completionBlock = {
if downloader.isCancelled {
return
}
}
pendingOperations.downloadsInProgress.removeValue(forKey: indexPath)
pendingOperations.downloadsInProgress[indexPath] = downloader
pendingOperations.downloadQueue.addOperation(downloader)
}
}
func suspendAllOperation() {
pendingOperations.downloadQueue.isSuspended = true
}
func resumeAllOperation() {
pendingOperations.downloadQueue.isSuspended = false
}
func loadImages() {
let pathArray = collectionView.indexPathsForVisibleItems
let allPendingOperations =
Set(Array(pendingOperations.downloadsInProgress.keys))
let visiblePath = Set(pathArray!)
var cancelOperation = allPendingOperations
cancelOperation.subtract(visiblePath)
var startOperation = visiblePath
startOperation.subtract(allPendingOperations)
for indexpath in cancelOperation {
if let pendingDownload =
pendingOperations.downloadsInProgress[indexpath] {
pendingDownload.cancel()
}
pendingOperations.downloadsInProgress.removeValue(forKey: indexpath)
}
for indexpath in startOperation {
let indexPath = indexpath
startDownload(imageId, indexPath: indexPath)
}
}
4) Fourth steps to implement the scrollView delegate methods.
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
suspendAllOperation()
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
loadImages()
resumeAllOperation()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
loadImages()
resumeAllOperation()
}
}
func clearImageDownloaderOperations() {
if pendingOperations.downloadsInProgress.count > 0 {
pendingOperations.downloadsInProgress.removeAll()
pendingOperations.downloadQueue.cancelAllOperations()
}
}
Using Kingfisher 5 for prefetching, loading, showing and caching images we can solve the issue.
let url = URL(fileURLWithPath: path)
let provider = LocalFileImageDataProvider(fileURL: url)
imageView.kf.setImage(with: provider)
https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet#image-from-local-file