Home > OS >  How to create a class that shows multiple UIImageViews in a line from an Array?
How to create a class that shows multiple UIImageViews in a line from an Array?

Time:09-24

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:

enter image description here

Expected Behavior:

enter image description here

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")
    }

}
  • Related