iOS/WAStickersThirdParty/StickerPackViewController.swift (230 lines of code) (raw):

// // Copyright (c) WhatsApp Inc. and its affiliates. // All rights reserved. // // This source code is licensed under the BSD-style license found in the // LICENSE file in the root directory of this source tree. // import UIKit class StickerPackViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout { @IBOutlet private weak var stickerPackPublisherLabel: UILabel! @IBOutlet private weak var stickerPackAnimationIcon: UIImageView! @IBOutlet private weak var stickersCollectionView: UICollectionView! @IBOutlet private weak var bottomCollectionViewConstraint: NSLayoutConstraint! private let topMarginInset: CGFloat = 5.0 private let marginInset: CGFloat = 22 private let interimMargin: CGFloat = 16.0 private var itemsPerRow: Int = 4 private let portraitItems = 4 private let landscapeItems = 8 private var portraitConstraints: [NSLayoutConstraint] = [] private var landscapeConstraints: [NSLayoutConstraint] = [] private var bottomGradientView: GradientView = GradientView(topColor: UIColor.white.withAlphaComponent(0.0), bottomColor: UIColor.white) private var topDivider: UIView = UIView() private var portraitOrientation: Bool { let currentOrientation = UIDevice.current.orientation return currentOrientation == .portrait || currentOrientation == .faceUp || currentOrientation == .faceDown || currentOrientation == .portraitUpsideDown } var stickerPack: StickerPack! override func viewDidLoad() { super.viewDidLoad() if #available(iOS 11.0, *) { navigationItem.largeTitleDisplayMode = .always } if #available(iOS 10.0, *) { stickersCollectionView.isPrefetchingEnabled = false } stickersCollectionView.register(StickerCell.self, forCellWithReuseIdentifier: "StickerCell") stickersCollectionView.scrollIndicatorInsets.bottom = 10 itemsPerRow = portraitOrientation ? portraitItems : landscapeItems let infoButton: UIButton = UIButton(type: .infoLight) infoButton.addTarget(self, action: #selector(infoPressed(button:)), for: .touchUpInside) navigationItem.rightBarButtonItem = UIBarButtonItem(customView: infoButton) bottomGradientView.isUserInteractionEnabled = false bottomGradientView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(bottomGradientView) topDivider.isUserInteractionEnabled = false topDivider.backgroundColor = UIColor(red: 164.0/255.0, green: 164.0/255.0, blue: 164.0/255.0, alpha: 1.0).withAlphaComponent(0.2) topDivider.alpha = 0.0 topDivider.translatesAutoresizingMaskIntoConstraints = false view.addSubview(topDivider) let buttonImage: UIImage = UIImage(named: "WALogoIcon")!.withRenderingMode(.alwaysTemplate) let shareImage: UIImage = UIImage(named: "ShareIcon")!.withRenderingMode(.alwaysTemplate) let addButton: AquaButton = AquaButton(frame: .zero) addButton.setTitle("Add to WhatsApp", for: .normal) addButton.setImage(buttonImage, for: .normal) addButton.addTarget(self, action: #selector(addButtonPressed(button:)), for: .touchUpInside) addButton.translatesAutoresizingMaskIntoConstraints = false addButton.isEnabled = Interoperability.canSend() view.addSubview(addButton) let shareButton: GrayRoundedButton = GrayRoundedButton(frame: .zero) shareButton.setTitle("Share", for: .normal) shareButton.setImage(shareImage, for: .normal) shareButton.addTarget(self, action: #selector(shareButtonPressed(button:)), for: .touchUpInside) shareButton.translatesAutoresizingMaskIntoConstraints = false view.addSubview(shareButton) stickerPackPublisherLabel.text = "\(stickerPack.publisher) • \(stickerPack.formattedSize)" stickerPackAnimationIcon.isHidden = !stickerPack.animated let tapGuideLabel: UILabel = UILabel() tapGuideLabel.text = "Tap on any sticker to copy or share it" tapGuideLabel.font = UIFont.systemFont(ofSize: 15) tapGuideLabel.textColor = UIColor(red: 0.6, green: 0.6, blue: 0.6, alpha: 1.0) tapGuideLabel.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tapGuideLabel) let buttonSize: CGSize = CGSize(width: 280.0, height: 50.0) let buttonLandscapeSize: CGSize = CGSize(width: 250.0, height: 50.0) let buttonBottomMargin: CGFloat = 20.0 guard let view = view else { return } // Share button constraints view.addConstraint(NSLayoutConstraint(item: view, attribute: .bottomMargin, relatedBy: .equal, toItem: shareButton, attribute: .bottom, multiplier: 1.0, constant: buttonBottomMargin)) let centerPortraitShareConstraint = NSLayoutConstraint(item: view, attribute: .centerXWithinMargins, relatedBy: .equal, toItem: shareButton, attribute: .centerXWithinMargins, multiplier: 1.0, constant: 0.0) let centerLandscapeShareConstraint = NSLayoutConstraint(item: view, attribute: .centerXWithinMargins, relatedBy: .equal, toItem: shareButton, attribute: .centerXWithinMargins, multiplier: 1.0, constant: -buttonSize.width / 2.0 - 5.0) portraitConstraints.append(centerPortraitShareConstraint) landscapeConstraints.append(centerLandscapeShareConstraint) view.addConstraint(centerPortraitShareConstraint) view.addConstraint(centerLandscapeShareConstraint) let widthPortraitShareConstraint = NSLayoutConstraint(item: shareButton, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1.0, constant: buttonSize.width) let widthLandscapeShareConstraint = NSLayoutConstraint(item: shareButton, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1.0, constant: buttonLandscapeSize.width) shareButton.addConstraint(widthPortraitShareConstraint) shareButton.addConstraint(widthLandscapeShareConstraint) portraitConstraints.append(widthPortraitShareConstraint) landscapeConstraints.append(widthLandscapeShareConstraint) shareButton.addConstraint(NSLayoutConstraint(item: shareButton, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1.0, constant: buttonSize.height)) // Add button constraints let bottomPortraitAddConstraint = NSLayoutConstraint(item: shareButton, attribute: .top, relatedBy: .equal, toItem: addButton, attribute: .bottom, multiplier: 1.0, constant: 7.0) let bottomLandscapeAddConstraint = NSLayoutConstraint(item: view, attribute: .bottomMargin, relatedBy: .equal, toItem: addButton, attribute: .bottom, multiplier: 1.0, constant: buttonBottomMargin) portraitConstraints.append(bottomPortraitAddConstraint) landscapeConstraints.append(bottomLandscapeAddConstraint) view.addConstraint(bottomPortraitAddConstraint) view.addConstraint(bottomLandscapeAddConstraint) let centerPortraitAddConstraint = NSLayoutConstraint(item: view, attribute: .centerXWithinMargins, relatedBy: .equal, toItem: addButton, attribute: .centerXWithinMargins, multiplier: 1.0, constant: 0.0) let centerLandscapeAddConstraint = NSLayoutConstraint(item: view, attribute: .centerXWithinMargins, relatedBy: .equal, toItem: addButton, attribute: .centerXWithinMargins, multiplier: 1.0, constant: buttonSize.width / 2.0 + 5.0) portraitConstraints.append(centerPortraitAddConstraint) landscapeConstraints.append(centerLandscapeAddConstraint) view.addConstraint(centerPortraitAddConstraint) view.addConstraint(centerLandscapeAddConstraint) let widthPortraitAddConstraint = NSLayoutConstraint(item: addButton, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1.0, constant: buttonSize.width) let widthLandscapeAddConstraint = NSLayoutConstraint(item: addButton, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1.0, constant: buttonLandscapeSize.width) addButton.addConstraint(widthPortraitAddConstraint) addButton.addConstraint(widthLandscapeAddConstraint) portraitConstraints.append(widthPortraitAddConstraint) landscapeConstraints.append(widthLandscapeAddConstraint) addButton.addConstraint(NSLayoutConstraint(item: addButton, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1.0, constant: buttonSize.height)) // Tap guide label constraints view.addConstraint(NSLayoutConstraint(item: addButton, attribute: .top, relatedBy: .equal, toItem: tapGuideLabel, attribute: .bottom, multiplier: 1.0, constant: 14.0)) view.addConstraint(NSLayoutConstraint(item: tapGuideLabel, attribute: .centerXWithinMargins, relatedBy: .equal, toItem: view, attribute: .centerXWithinMargins, multiplier: 1.0, constant: 0.0)) view.addConstraint(NSLayoutConstraint(item: tapGuideLabel, attribute: .leading, relatedBy: .greaterThanOrEqual, toItem: view, attribute: .leading, multiplier: 1.0, constant: 0.0)) view.addConstraint(NSLayoutConstraint(item: view, attribute: .trailing, relatedBy: .greaterThanOrEqual, toItem: tapGuideLabel, attribute: .trailing, multiplier: 1.0, constant: 0.0)) // Collection view constraint var bottomCollectionViewConstraint = self.bottomCollectionViewConstraint! view.removeConstraint(bottomCollectionViewConstraint) bottomCollectionViewConstraint = NSLayoutConstraint(item: tapGuideLabel, attribute: .top, relatedBy: .equal, toItem: stickersCollectionView, attribute: .bottom, multiplier: 1.0, constant: 10.0) view.addConstraint(bottomCollectionViewConstraint) // Gradient constraints view.addConstraint(NSLayoutConstraint(item: bottomGradientView, attribute: .bottom, relatedBy: .equal, toItem: stickersCollectionView, attribute: .bottom, multiplier: 1.0, constant: 0.0)) view.addConstraint(NSLayoutConstraint(item: bottomGradientView, attribute: .centerXWithinMargins, relatedBy: .equal, toItem: view, attribute: .centerXWithinMargins, multiplier: 1.0, constant: 0.0)) view.addConstraint(NSLayoutConstraint(item: bottomGradientView, attribute: .width, relatedBy: .equal, toItem: stickersCollectionView, attribute: .width, multiplier: 1.0, constant: 0.0)) bottomGradientView.addConstraint(NSLayoutConstraint(item: bottomGradientView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1.0, constant: 50.0)) // Top divider constraints view.addConstraint(NSLayoutConstraint(item: topDivider, attribute: .top, relatedBy: .equal, toItem: stickersCollectionView, attribute: .top, multiplier: 1.0, constant: 0.0)) view.addConstraint(NSLayoutConstraint(item: topDivider, attribute: .leading, relatedBy: .equal, toItem: view, attribute: .leading, multiplier: 1.0, constant: 0.0)) view.addConstraint(NSLayoutConstraint(item: topDivider, attribute: .trailing, relatedBy: .equal, toItem: view, attribute: .trailing, multiplier: 1.0, constant: 0.0)) topDivider.addConstraint(NSLayoutConstraint(item: topDivider, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1.0, constant: 1.0)) changeConstraints() } private func changeConstraints() { portraitConstraints.forEach { $0.isActive = portraitOrientation } landscapeConstraints.forEach { $0.isActive = !portraitOrientation } } override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) itemsPerRow = portraitOrientation ? portraitItems : landscapeItems changeConstraints() } // MARK: Scrollview func scrollViewDidScroll(_ scrollView: UIScrollView) { if scrollView.contentOffset.y > topMarginInset { topDivider.alpha = 1.0 } else { topDivider.alpha = 0.0 } } func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { if scrollView.contentOffset.y > topMarginInset { topDivider.alpha = 1.0 } else { topDivider.alpha = 0.0 } } // MARK: Collectionview func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat { return interimMargin } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets { return UIEdgeInsets(top: topMarginInset, left: marginInset, bottom: 60, right: marginInset) } func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { let length = (collectionView.bounds.size.width - marginInset * 2 - interimMargin * CGFloat(itemsPerRow - 1)) / CGFloat(itemsPerRow) return CGSize(width: length, height: length) } func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { return stickerPack.stickers.count } func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "StickerCell", for: indexPath) as! StickerCell cell.sticker = stickerPack.stickers[indexPath.row] return cell } func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { let sticker: Sticker = stickerPack.stickers[indexPath.item] let cell = collectionView.cellForItem(at: indexPath) showActionSheet(withSticker: sticker, overCell: cell!) } // MARK: Targets func showActionSheet(withSticker sticker: Sticker, overCell cell: UICollectionViewCell) { var emojisString: String? #if DEBUG if let emojis = sticker.emojis { emojisString = emojis.joined(separator: " ") } #endif let actionSheet: UIAlertController = UIAlertController(title: "\n\n\n\n\n\n\n", message: emojisString, preferredStyle: .actionSheet) actionSheet.popoverPresentationController?.sourceView = cell.contentView actionSheet.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection() actionSheet.popoverPresentationController?.sourceRect = CGRect(x: cell.contentView.bounds.midX, y: cell.contentView.bounds.midY, width: 0, height: 0) actionSheet.addAction(UIAlertAction(title: "Copy to Clipboard", style: .default, handler: { _ in sticker.copyToPasteboardAsImage() })) actionSheet.addAction(UIAlertAction(title: "Share via", style: .default, handler: { _ in self.showShareSheet(withSticker: sticker) })) actionSheet.addAction(UIAlertAction(title: "Cancel", style: .cancel)) if let stickerImage = sticker.imageData.image { actionSheet.addImageView(withImage: stickerImage, animated: sticker.imageData.animated) } present(actionSheet, animated: true) } func showShareSheet(withSticker sticker: Sticker) { guard let image = sticker.imageData.image else { return } let shareViewController: UIActivityViewController = UIActivityViewController(activityItems: [image], applicationActivities: nil) shareViewController.popoverPresentationController?.sourceView = self.view shareViewController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection() shareViewController.popoverPresentationController?.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0) present(shareViewController, animated: true) } @objc func infoPressed(button: UIButton) { performSegue(withIdentifier: "info", sender: self) } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let nc = segue.destination as? UINavigationController, let infoVC = nc.viewControllers.first as? StickerPackInfoViewController { infoVC.stickerPack = stickerPack } } @objc func addButtonPressed(button: AquaButton) { let loadingAlert: UIAlertController = UIAlertController(title: "Sending to WhatsApp", message: "\n\n", preferredStyle: .alert) loadingAlert.addSpinner() present(loadingAlert, animated: true) stickerPack.sendToWhatsApp { completed in loadingAlert.dismiss(animated: true) } } @objc func shareButtonPressed(button: GrayRoundedButton) { var stickerImages: [UIImage] = [] for sticker in stickerPack.stickers { if let image = sticker.imageData.image { stickerImages.append(image) } } let activityViewController: UIActivityViewController = UIActivityViewController(activityItems: stickerImages, applicationActivities: nil) let parentView = button as UIView activityViewController.popoverPresentationController?.sourceView = parentView activityViewController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection() activityViewController.popoverPresentationController?.sourceRect = CGRect(x: parentView.bounds.midX, y: parentView.bounds.midY, width: 0, height: 0) present(activityViewController, animated: true) } }