import PromiseKit import NVActivityIndicatorView final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { private let maxWidth: CGFloat private var rooms: [OpenGroupAPIV2.Info] = [] { didSet { update() } } private var heightConstraint: NSLayoutConstraint! var delegate: OpenGroupSuggestionGridDelegate? // MARK: UI Components private lazy var layout: UICollectionViewFlowLayout = { let result = UICollectionViewFlowLayout() result.minimumLineSpacing = 0 result.minimumInteritemSpacing = 0 return result }() private lazy var collectionView: UICollectionView = { let result = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout) result.register(Cell.self, forCellWithReuseIdentifier: Cell.identifier) result.backgroundColor = .clear result.isScrollEnabled = false result.dataSource = self result.delegate = self return result }() private lazy var spinner: NVActivityIndicatorView = { let result = NVActivityIndicatorView(frame: CGRect.zero, type: .circleStrokeSpin, color: Colors.text, padding: nil) result.set(.width, to: OpenGroupSuggestionGrid.cellHeight) result.set(.height, to: OpenGroupSuggestionGrid.cellHeight) return result }() // MARK: Settings private static let cellHeight: CGFloat = 40 private static let separatorWidth = 1 / UIScreen.main.scale // MARK: Initialization init(maxWidth: CGFloat) { self.maxWidth = maxWidth super.init(frame: CGRect.zero) initialize() } override init(frame: CGRect) { preconditionFailure("Use init(maxWidth:) instead.") } required init?(coder: NSCoder) { preconditionFailure("Use init(maxWidth:) instead.") } private func initialize() { addSubview(collectionView) collectionView.pin(to: self) addSubview(spinner) spinner.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top ], to: self) spinner.startAnimating() heightConstraint = set(.height, to: OpenGroupSuggestionGrid.cellHeight) widthAnchor.constraint(greaterThanOrEqualToConstant: OpenGroupSuggestionGrid.cellHeight).isActive = true if OpenGroupAPIV2.defaultRoomsPromise == nil { OpenGroupAPIV2.getDefaultRoomsIfNeeded() } let _ = OpenGroupAPIV2.defaultRoomsPromise?.done { [weak self] rooms in self?.rooms = rooms } } // MARK: Updating private func update() { spinner.stopAnimating() spinner.isHidden = true let roomCount = min(rooms.count, 8) // Cap to a maximum of 8 (4 rows of 2) let height = OpenGroupSuggestionGrid.cellHeight * ceil(CGFloat(roomCount) / 2) heightConstraint.constant = height collectionView.reloadData() } // MARK: Layout func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { return CGSize(width: maxWidth / 2, height: OpenGroupSuggestionGrid.cellHeight) } // MARK: Data Source func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return min(rooms.count, 8) // Cap to a maximum of 8 (4 rows of 2) } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: Cell.identifier, for: indexPath) as! Cell cell.room = rooms[indexPath.item] return cell } // MARK: Interaction func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let room = rooms[indexPath.item] delegate?.join(room) } } // MARK: Cell extension OpenGroupSuggestionGrid { fileprivate final class Cell : UICollectionViewCell { var room: OpenGroupAPIV2.Info? { didSet { update() } } static let identifier = "OpenGroupSuggestionGridCell" private lazy var snContentView: UIView = { let result = UIView() result.backgroundColor = Colors.navigationBarBackground result.set(.height, to: Cell.contentViewHeight) result.layer.cornerRadius = Cell.contentViewCornerRadius return result }() private lazy var imageView: UIImageView = { let result = UIImageView() let size: CGFloat = 24 result.set(.width, to: size) result.set(.height, to: size) result.layer.cornerRadius = size / 2 result.clipsToBounds = true return result }() private lazy var label: UILabel = { let result = UILabel() result.textColor = Colors.text result.font = .systemFont(ofSize: Values.smallFontSize) result.lineBreakMode = .byTruncatingTail return result }() private static let contentViewInset: CGFloat = 4 private static var contentViewHeight: CGFloat { OpenGroupSuggestionGrid.cellHeight - 2 * contentViewInset } private static var contentViewCornerRadius: CGFloat { contentViewHeight / 2 } override init(frame: CGRect) { super.init(frame: frame) setUpViewHierarchy() } required init?(coder: NSCoder) { super.init(coder: coder) setUpViewHierarchy() } private func setUpViewHierarchy() { addSubview(snContentView) let stackView = UIStackView(arrangedSubviews: [ imageView, label ]) stackView.axis = .horizontal stackView.spacing = Values.smallSpacing snContentView.addSubview(stackView) stackView.center(.vertical, in: snContentView) stackView.pin(.leading, to: .leading, of: snContentView, withInset: 4) snContentView.trailingAnchor.constraint(greaterThanOrEqualTo: stackView.trailingAnchor, constant: Values.smallSpacing).isActive = true snContentView.pin(to: self, withInset: Cell.contentViewInset) } override func layoutSubviews() { super.layoutSubviews() let newPath = UIBezierPath(roundedRect: snContentView.bounds, cornerRadius: Cell.contentViewCornerRadius).cgPath snContentView.layer.shadowPath = newPath snContentView.layer.shadowColor = UIColor.black.cgColor snContentView.layer.shadowOffset = CGSize.zero snContentView.layer.shadowOpacity = isLightMode ? 0.2 : 0.6 snContentView.layer.shadowRadius = 2 } private func update() { guard let room = room else { return } let promise = OpenGroupAPIV2.getGroupImage(for: room.id, on: OpenGroupAPIV2.defaultServer) imageView.image = given(promise.value) { UIImage(data: $0)! } imageView.isHidden = (imageView.image == nil) label.text = room.name } } } // MARK: Delegate protocol OpenGroupSuggestionGridDelegate { func join(_ room: OpenGroupAPIV2.Info) }