I am running into a weird bug while trying to create a visual element within collection view cells. The class takes an array of UserModels and aligns all the profile pictures in a line. If there are more than 4 users in an array the last image view is a blurred image with a ' N' to let users know there are more users that can't fit on screen.
Actual Behavior:
Expected Behavior:
The image corrects itself once you go to another screen and come back.
This is the class I've created to get the following result.
import Foundation
import UIKit
class UserStack: UIView{
var userArray: [UserModel]!
var size: CGFloat = 50
init(userArray: [UserModel]){
super.init(frame: .zero)
self.userArray = userArray
if self.userArray.count <= 4{
fourOrLess()
}else{
fiveOrMore()
}
}
func fourOrLess(){
var spacing: CGFloat = 0
userArray.forEach { user in
let imageView = UIImageView()
guard let url = user.imageURL else {return}
imageView.sd_setImage(with: URL(string: url)) { i, e, c, u in
if let e = e{
print("DEBUG: Error setting userStack - \(e)")
return
}
self.addSubview(imageView)
self.bringSubviewToFront(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = self.size * 0.4
imageView.layer.masksToBounds = true
imageView.layer.borderWidth = 0.75
imageView.layer.borderColor = UIColor.white.cgColor
///LAYOUT
imageView.setDimensions(height: self.size, width: self.size)
imageView.centerY(inView: self)
imageView.anchor(left: self.leftAnchor, paddingLeft: spacing)
//DispatchQueue.main.asyncAfter(deadline: .now() 0.01) {
spacing = (self.size - 10)
// }
}
}
}
func fiveOrMore(){
var count: Int = 1
var spacing: CGFloat = 0
userArray.forEach { user in
if count < 4{
count = 1
let imageView = UIImageView()
guard let url = user.imageURL else {return}
imageView.sd_setImage(with: URL(string: url)) { i, e, c, u in
if let e = e{
print("DEBUG: Error setting userStack - \(e)")
return
}
self.addSubview(imageView)
self.bringSubviewToFront(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = self.size * 0.4
imageView.layer.masksToBounds = true
imageView.layer.borderWidth = 0.75
imageView.layer.borderColor = UIColor.white.cgColor
///LAYOUT
imageView.setDimensions(height: self.size, width: self.size)
imageView.centerY(inView: self)
imageView.anchor(left: self.leftAnchor, paddingLeft: spacing)
spacing = (self.size - 10)
}
}else{
///NUMBER VIEW
let numberImageView = UIImageView()
let blurView = UIView()
let numberLabel = UILabel()
guard let _URL = userArray.last?.imageURL else {return}
numberImageView.sd_setImage(with: URL(string: _URL)) { i, e, c, u in
if let e = e{
print("DEBUG: Error setting userStack - \(e)")
return
}
self.addSubview(numberImageView)
numberImageView.translatesAutoresizingMaskIntoConstraints = false
numberImageView.contentMode = .scaleAspectFill
numberImageView.layer.cornerRadius = self.size * 0.4
numberImageView.layer.masksToBounds = true
//numberImageView.layer.borderWidth = 0.75
//numberImageView.layer.borderColor = UIColor.white.cgColor
numberImageView.addSubview(blurView)
blurView.translatesAutoresizingMaskIntoConstraints = false
blurView.backgroundColor = .init(white: 0.2, alpha: 0.9)
blurView.layer.cornerRadius = self.size * 0.4
blurView.layer.masksToBounds = true
blurView.layer.borderWidth = 0.75
blurView.layer.borderColor = UIColor.white.cgColor
blurView.addSubview(numberLabel)
numberLabel.text = " \(self.userArray.count - 3)"
numberLabel.font = .poppinsMedium(size: 14)
numberLabel.textColor = .white
///LAYOUT
numberImageView.setDimensions(height: self.size, width: self.size)
numberImageView.centerY(inView: self)
numberImageView.anchor(left: self.leftAnchor, paddingLeft: spacing)
numberImageView.bringSubviewToFront(blurView)
blurView.setDimensions(height: self.size, width: self.size)
blurView.center(inView: numberImageView)
numberLabel.center(inView: blurView)
}
}
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
UIHelper Functions:
extension UIView {
func anchor(top: NSLayoutYAxisAnchor? = nil,
left: NSLayoutXAxisAnchor? = nil,
bottom: NSLayoutYAxisAnchor? = nil,
right: NSLayoutXAxisAnchor? = nil,
paddingTop: CGFloat = 0,
paddingLeft: CGFloat = 0,
paddingBottom: CGFloat = 0,
paddingRight: CGFloat = 0,
width: CGFloat? = nil,
height: CGFloat? = nil) {
translatesAutoresizingMaskIntoConstraints = false
if let top = top {
topAnchor.constraint(equalTo: top, constant: paddingTop).isActive = true
}
if let left = left {
leftAnchor.constraint(equalTo: left, constant: paddingLeft).isActive = true
}
if let bottom = bottom {
bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom).isActive = true
}
if let right = right {
rightAnchor.constraint(equalTo: right, constant: -paddingRight).isActive = true
}
if let width = width {
widthAnchor.constraint(equalToConstant: width).isActive = true
}
if let height = height {
heightAnchor.constraint(equalToConstant: height).isActive = true
}
}
func center(inView view: UIView, yConstant: CGFloat? = 0) {
translatesAutoresizingMaskIntoConstraints = false
centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: yConstant!).isActive = true
}
func centerX(inView view: UIView, topAnchor: NSLayoutYAxisAnchor? = nil, paddingTop: CGFloat? = 0) {
translatesAutoresizingMaskIntoConstraints = false
centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
if let topAnchor = topAnchor {
self.topAnchor.constraint(equalTo: topAnchor, constant: paddingTop!).isActive = true
}
}
func centerY(inView view: UIView, leftAnchor: NSLayoutXAxisAnchor? = nil,
paddingLeft: CGFloat = 0, constant: CGFloat = 0) {
translatesAutoresizingMaskIntoConstraints = false
centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: constant).isActive = true
if let left = leftAnchor {
anchor(left: left, paddingLeft: paddingLeft)
}
}
func setDimensions(height: CGFloat, width: CGFloat) {
translatesAutoresizingMaskIntoConstraints = false
heightAnchor.constraint(equalToConstant: height).isActive = true
widthAnchor.constraint(equalToConstant: width).isActive = true
}
}
How do I make this class more dependable so I can get the expected result every time?
I'm thinking perhaps make an array of UIImageView and return it to my collection view cell.
CodePudding user response:
You shouldn't call async methods in a loop without some form of synchronization otherwise you will have strange side effects. I am willing to bet this is the reason you have inconsistent results.
Download all your images first and then display them in your view.
You can use combine to fetch all the images really easily.
struct ImageLoader {
let urls: [URL]
func publish() -> AnyPublisher<[UIImage], Error> {
urls.publisher
.flatMap {
URLSession.shared.dataTaskPublisher(for: $0)
}
.mapError { $0 as Error }
.compactMap { $0.data }
.compactMap { UIImage(data: $0) }
.collect()
.eraseToAnyPublisher()
}
}
You can use the ImageLoader like so:
var subscriptions = Set<AnyCancellable>()
func fetchImages(_ urls: [URL], completion: @escaping ([UIImage]) -> Void) {
ImageLoader(urls: urls)
.publish()
.receive(on: DispatchQueue.main)
.sink { result in
print("Result: \(result)")
} receiveValue: { images in
completion(images)
}
.store(in: &subscriptions)
}
As for your UserStack you can do something like this:
class UserStack2: UIView {
var size: CGFloat = 50
init(images: [UIImage]) {
super.init(frame: .zero)
addImages(images)
}
func createImageView(with image: UIImage, spacing: CGFloat) {
let imageView = UIImageView(image: image)
self.addSubview(imageView)
self.bringSubviewToFront(imageView)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.contentMode = .scaleAspectFill
imageView.layer.cornerRadius = self.size * 0.4
imageView.layer.masksToBounds = true
imageView.layer.borderWidth = 0.75
imageView.layer.borderColor = UIColor.white.cgColor
imageView.setDimensions(height: self.size, width: self.size)
imageView.centerY(inView: self)
imageView.anchor(left: self.leftAnchor, paddingLeft: spacing)
}
func createPlaceHolderView(withValue value: Int, spacing: CGFloat) {
let numberImageView = UIImageView()
let blurView = UIView()
let numberLabel = UILabel()
self.addSubview(numberImageView)
numberImageView.translatesAutoresizingMaskIntoConstraints = false
numberImageView.contentMode = .scaleAspectFill
numberImageView.layer.cornerRadius = self.size * 0.4
numberImageView.layer.masksToBounds = true
//numberImageView.layer.borderWidth = 0.75
//numberImageView.layer.borderColor = UIColor.white.cgColor
numberImageView.addSubview(blurView)
blurView.translatesAutoresizingMaskIntoConstraints = false
blurView.backgroundColor = .init(white: 0.2, alpha: 0.9)
blurView.layer.cornerRadius = self.size * 0.4
blurView.layer.masksToBounds = true
blurView.layer.borderWidth = 0.75
blurView.layer.borderColor = UIColor.white.cgColor
blurView.addSubview(numberLabel)
numberLabel.text = " \(value)"
// numberLabel.font = .poppinsMedium(size: 14)
numberLabel.textColor = .white
numberImageView.setDimensions(height: self.size, width: self.size)
numberImageView.centerY(inView: self)
numberImageView.anchor(left: self.leftAnchor, paddingLeft: spacing)
numberImageView.bringSubviewToFront(blurView)
blurView.setDimensions(height: self.size, width: self.size)
blurView.center(inView: numberImageView)
numberLabel.center(inView: blurView)
}
func addImages(_ images: [UIImage]) {
let maxImages = 4
var spacing: CGFloat = 0
images.prefix(maxImages).forEach { image in
createImageView(with: image, spacing: spacing)
spacing = (self.size - 10)
}
if images.count > maxImages {
createPlaceHolderView(withValue: images.count - maxImages, spacing: spacing)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}