Trouble getting chart object to work in custom UIView - ios

I'm using Daniel Gindi's Charts library (iOS-charts), and in order to keep things simple I'm using a custom UIView to store the chart view (as a ChartViewBase). I have a class hierarchy of View Controller classes where the leaf class has the reference to the UIView with the ChartViewBase in it. The problem is that the chart is refusing to display, all I get is a black rectangle where the chart is supposed to be. I guess the code would probably be the most useful, so here goes nothing:
The generic UIView chart-holding class:
import UIKit
import Charts
protocol GenericChartViewDelegate: class {
func backButtonTapped()
func switchChartButtonTapped()
func finalizeResultsButtonTapped()
}
class GenericChartView: UIView, ChartViewDelegate
{
#IBOutlet weak var headerDisplay: UIButton!
#IBOutlet var view: UIView!
weak var delegate: GenericChartViewDelegate?
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
Bundle.main.loadNibNamed("GenericChartView", owner: self, options: nil)
self.addSubview(self.view)
chart.delegate = self
}
override init(frame: CGRect)
{
super.init(frame: frame)
Bundle.main.loadNibNamed("GenericChartView", owner: self, options: nil)
self.addSubview(self.view)
chart.delegate = self
}
#IBOutlet var chart: ChartViewBase!
#IBAction func BackButton(_ sender: UIButton) {
delegate?.backButtonTapped()
}
#IBAction func SwitchChartButton(_ sender: UIButton) {
delegate?.switchChartButtonTapped()
}
#IBAction func FinalizeResultsButton(_ sender: UIButton) {
delegate?.finalizeResultsButtonTapped()
}
func setHeader(header: String)
{
headerDisplay.setTitle(header, for: UIControlState.normal)
headerDisplay.layer.cornerRadius = MyVariables.borderCurvature
headerDisplay.titleLabel?.adjustsFontSizeToFitWidth = true
}
}
Now the base chart view controller:
class ChartViewControllerBase: UIViewController
{
var myColors : [NSUIColor] = []
//var bounds : CGRect!
override func viewDidLoad() {
super.viewDidLoad()
let sessionCount = ShareData.sharedInstance.currentSessionNumber
//NotificationCenter.default.addObserver(self, selector: "rotated", name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
createChart()
var myBase = getChartObject()
var myChartView = getChartDisplayView()
setDelegate()
if let myName = ShareData.sharedInstance.currentAccount.name {//sessionName.characters.last!
let myTitle = "Session " + "\(sessionCount)" + " Results - " + myName
myChartView.setHeader(header: myTitle)
}
//chartOriginalBounds = chart.bounds
if let resultChoice = ShareData.sharedInstance.sessionDataObjectContainer[ShareData.sharedInstance.currentAccountSessionNames[sessionCount - 1]]?.chartInfo
{
makeChart(resultChoice: resultChoice)
}
// Do any additional setup after loading the view.
}
func setDelegate()
{
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func createChart()
{
}
func getChartDisplayView() -> GenericChartView
{
return GenericChartView(frame: self.view.bounds)
}
func getChartObject() -> ChartViewBase
{
return ChartViewBase()
}
func makeChart(resultChoice: chartData)
{
}
func getColors(value: Double)
{
}
func getChartDataEntry(xval: Double, yval: Double) -> ChartDataEntry
{
return ChartDataEntry(x: xval, y: yval)
}
}
Now the LineAndBarChart code:
class LineAndBarChartViewControllerBase: ChartViewControllerBase {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func getChartObject() -> ChartViewBase
{
return ChartViewBase()
}
var yAxis: YAxis!
func setYAxisFormatter() {
}
func createSet(myEntries: [ChartDataEntry])
{
}
override func makeChart(resultChoice: chartData)
{
let chart = getChartObject() as! BarLineChartViewBase
chart.chartDescription?.enabled = false
chart.drawGridBackgroundEnabled = false
chart.dragEnabled = true
chart.setScaleEnabled(true)
chart.pinchZoomEnabled = true
let xAxis = chart.xAxis
xAxis.labelPosition = XAxis.LabelPosition.bottom
chart.rightAxis.enabled = false
xAxis.drawGridLinesEnabled = false
xAxis.drawAxisLineEnabled = true
xAxis.granularity = 1.0
//xAxis.setValuesForKeys(<#T##keyedValues: [String : Any]##[String : Any]#>)
yAxis = chart.leftAxis
chart.rightAxis.enabled = false
chart.legend.enabled = true
yAxis.granularity = 1.0
yAxis.drawGridLinesEnabled = false
var counter = 1.0
var myEntries : [ChartDataEntry] = []
var myXValues : [String] = []
if !resultChoice.plotPoints.isEmpty
{
for var item in resultChoice.plotPoints
{
let timeString : String =
{
let formatter = DateFormatter()
formatter.dateFormat = "h:mm a"
formatter.amSymbol = "AM"
formatter.pmSymbol = "PM"
return formatter.string(from: item.xValue)
}()
myXValues.append(timeString)
/*if(item.showXValue)
{
myXValues.append(timeString)
}
else
{
myXValues.append("")
}*/
let myEntry = getChartDataEntry(xval: counter, yval: Double(item.yValue))
getColors(value: myEntry.y)
counter += 1
myEntries.append(myEntry)
}
let myFormatter = MyXAxisValueFormatter(values: myXValues)
xAxis.valueFormatter = myFormatter
setYAxisFormatter()
createSet(myEntries: myEntries)
}
}
}
class MyXAxisValueFormatter: NSObject, IAxisValueFormatter {
var mValues: [String]
init(values: [String]) {
self.mValues = values;
}
func stringForValue( _ value: Double, axis: AxisBase?) -> String
{
// "value" represents the position of the label on the axis (x or y)
let myVal: Int = Int(value)
if (myVal < mValues.count)
{
return mValues[myVal];
}
else
{
return "ERROR"
}
}
}
Then the bar chart base code:
class BarChartViewControllerBase: LineAndBarChartViewControllerBase {
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func createChart()
{
var myChart = getChartDisplayView()
myChart.chart = BarChartView(frame: myChart.chart.contentRect)
}
override func createSet(myEntries: [ChartDataEntry])
{
let chart = getChartObject() as! BarChartView
let mySet = BarChartDataSet(values: myEntries, label: "Values")
mySet.colors = myColors
mySet.valueColors = myColors
setValueFormatter(set: mySet)
let myBarChartData = BarChartData(dataSet: mySet)
myBarChartData.barWidth = 0.8
chart.data = myBarChartData
}
func setValueFormatter(set: BarChartDataSet)
{
}
override func getChartDataEntry(xval: Double, yval: Double) -> ChartDataEntry
{
return BarChartDataEntry(x: xval, y: yval)
}
}
And finally, the leaf (most derived class) code:
class DerivedClassChartViewController: BarChartViewControllerBase, GenericChartViewDelegate
{
#IBOutlet weak var chartView: GenericChartView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
override func getChartDisplayView() -> GenericChartView
{
return chartView
}
override func getChartObject() -> ChartViewBase
{
return chartView.chart
}
override func getColors(value: Double)
{
if (value == 0.0)
{
myColors.append(UIColor.black)
}
else if (value == 1.0)
{
myColors.append(MyVariables.colorOne)
}
else if (value == 2.0)
{
myColors.append(MyVariables.colorTwo)
}
else if (value == 3.0)
{
myColors.append(MyVariables.colorThree)
}
else if (value == 4.0)
{
myColors.append(MyVariables.colorFour)
}
else if (value == 5.0)
{
myColors.append(MyVariables.colorFive)
}
}
func backButtonTapped()
{
self.navigationController?.popViewController(animated: false)
}
func switchChartButtonTapped()
{
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: UIAlertControllerStyle.actionSheet)
let goToLineChartAction = UIAlertAction(title: "Line Chart", style: UIAlertActionStyle.default)
{ (alertAction: UIAlertAction!) -> Void in
self.navigationController?.popViewController(animated: false)
let viewControllerStoryboardId = "LineChartDisplayViewController"
let storyboardName = "Main"
let storyboard = UIStoryboard(name: storyboardName, bundle: Bundle.main)
let viewController = storyboard.instantiateViewController(withIdentifier: viewControllerStoryboardId)
self.navigationController?.pushViewController(viewController, animated: false)
}
alertController.addAction(goToLineChartAction)
alertController.view.tintColor = UIColor.black
present(alertController, animated: true, completion: nil)
let presentationController = alertController.popoverPresentationController
presentationController?.sourceView = self.view
presentationController?.sourceRect = CGRect(x: 330, y: 210, width: 330, height: 210)
}
func finalizeResultsButtonTapped()
{
}
override func setDelegate()
{
chartView.delegate = self
}
override func setValueFormatter(set: BarChartDataSet)
{
set.valueFormatter = DerivedClassNumberFormatter()
}
}
class DerivedClassNumberFormatter: NSObject, IValueFormatter {
func stringForValue(_ value: Double, entry: ChartDataEntry, dataSetIndex: Int, viewPortHandler: ViewPortHandler?) -> String {
var returnVal = "\(Int(value))"
return returnVal
}
}
I do have a few questions. Do you make the UIView the ChartViewDelegate, or should that be the view controller that holds the UIView that holds the chart object? Am I missing anything obvious? I've used custom UIViews before, so I know that that code is correct (aside from whether the class should be ChartViewDelegate or not, that is- that I don't know). Yes I do need all of these classes, as I will have many, many chart types in the future. Anyhow, any suggestions on what I can do to get the chart to display would be great.
Thanks,
Sean

I also am using the Charts library. I am not sure if I quite understand your limitation, but I was able to include Charts inside of a custom UIView. I have other methods (not shown) to pass actual data to the Labels and Charts from the View Controller.
Here's how I did it:
import UIKit
import Charts
class MyHeader: UIView, ChartViewDelegate {
// My Header is the top view of the ViewController
// It gives summary information for the user
// MARK: - Properties
var firstLabel = LabelView()
var secondLabel = LabelView()
var firstPie = PieChartView()
var secondPie = PieChartView()
var thirdPie = PieChartView()
let nformatter = NumberFormatter()
// MARK: - LifeCycle
override func awakeFromNib() {
super.awakeFromNib()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.white.withAlphaComponent(0.5)
setupStack()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Setup Methods
func setupLabels() {
let lbls = [firstLabel, secondLabel]
for lbl in lbls {
lbl.label.font = UIFont.preferredFont(forTextStyle: .title3)
lbl.translatesAutoresizingMaskIntoConstraints = false
lbl.heightAnchor.constraint(equalToConstant: 50).isActive = true
}
let pies = [firstPie, secondPie, thirdPie]
for pie in pies {
pie.translatesAutoresizingMaskIntoConstraints = false
pie.heightAnchor.constraint(equalToConstant: 100).isActive = true
pie.spanSuperView() // spanSuperView is a UIView extension that I use
pie.delegate = self as ChartViewDelegate
// entry label styling
pie.entryLabelColor = .white
pie.entryLabelFont = .systemFont(ofSize: 12, weight: .light)
pie.animate(xAxisDuration: 1.4, easingOption: .easeOutBack)
}
}
func setupFake() {
let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
let unitsSold = [20.0, 4.0, 6.0, 3.0, 12.0, 16.0]
setChart(dataPoints: months, values: unitsSold)
}
func setChart(dataPoints: [String], values: [Double]) {
var dataEntries: [ChartDataEntry] = []
for i in 0..<dataPoints.count {
let dataEntry = ChartDataEntry(x: values[i], y: Double(i))
dataEntries.append(dataEntry)
}
let set = PieChartDataSet(values: dataEntries, label: "Results")
set.drawIconsEnabled = false
set.sliceSpace = 2
set.colors = ChartColorTemplates.joyful()
+ ChartColorTemplates.colorful()
+ ChartColorTemplates.liberty()
+ ChartColorTemplates.pastel()
+ [UIColor(red: 51/255, green: 181/255, blue: 229/255, alpha: 1)]
let data = PieChartData(dataSet: set)
let pies = [firstPie, secondPie, thirdPie]
for pie in pies {
pie.data = data
}
}
func setupStack(){
setupFake()
setupLabels()
let dashStack = UIStackView(arrangedSubviews: [firstPie, secondPie, thirdPie])
dashStack.axis = .horizontal
dashStack.distribution = .fillEqually
dashStack.alignment = .center
dashStack.spacing = 0
dashStack.translatesAutoresizingMaskIntoConstraints = false
let stackView = UIStackView(arrangedSubviews: [firstLabel, secondLabel, dashStack])
stackView.axis = .vertical
stackView.distribution = .fill
stackView.alignment = .fill
stackView.spacing = 0
stackView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(stackView)
//autolayout the stack view
stackView.spanSuperView()
}
}

Related

How can I set up a collection view of AVPlayers to only play a video in the current selected cell?

I have a UICollectionView setup that has cells of video posts from my database. Right now, when the collection view is loaded, all of the videos in the different cells start playing. I want the videos to not play in any cells except the selected cell so that the video audios don't play over each other. How can I do this? Here is the code...
The view controller:
import UIKit
import Photos
struct VideoModel {
let username: String
let videoFileURL: String
}
class BetaClipsViewController: UIViewController, UICollectionViewDelegate {
private var collectionView: UICollectionView?
private var data = [VideoModel]()
/// Notification observer
private var observer: NSObjectProtocol?
/// All post models
private var allClips: [(clip: Clip, owner: String)] = []
private var viewModels = [[ClipFeedCellType]]()
override func viewDidLoad() {
super.viewDidLoad()
title = ""
// for _ in 0..<10 {
// let model = VideoModel(username: "#CJMJM",
// videoFileURL: "https://firebasestorage.googleapis.com:443/v0/b/globe-e8b7f.appspot.com/o/clipvideos%2F1637024382.mp4?alt=media&token=c12d0481-f834-4a17-8eee-30595bdf0e8b")
// data.append(model)
// }
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.itemSize = CGSize(width: view.frame.size.width,
height: view.frame.size.height)
layout.sectionInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
collectionView?.register(ClipsCollectionViewCell.self,
forCellWithReuseIdentifier: ClipsCollectionViewCell.identifier)
collectionView?.isPagingEnabled = true
collectionView?.delegate = self
collectionView?.dataSource = self
view.addSubview(collectionView!)
fetchClips()
observer = NotificationCenter.default.addObserver(
forName: .didPostNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.viewModels.removeAll()
self?.fetchClips()
}
self.collectionView?.reloadData()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView?.frame = view.bounds
}
private func fetchClips() {
// guard let username = UserDefaults.standard.string(forKey: "username") else {
// return
// }
let userGroup = DispatchGroup()
userGroup.enter()
var allClips: [(clip: Clip, owner: String)] = []
DatabaseManager.shared.clips() { result in
DispatchQueue.main.async {
defer {
userGroup.leave()
}
switch result {
case .success(let clips):
allClips.append(contentsOf: clips.compactMap({
(clip: $0, owner: $0.owner)
}))
case .failure:
break
}
}
}
userGroup.notify(queue: .main) {
let group = DispatchGroup()
self.allClips = allClips
allClips.forEach { model in
group.enter()
self.createViewModel(
model: model.clip,
username: model.owner,
completion: { success in
defer {
group.leave()
}
if !success {
print("failed to create VM")
}
}
)
}
group.notify(queue: .main) {
self.sortData()
self.collectionView?.reloadData()
}
}
}
private func sortData() {
allClips = allClips.shuffled()
viewModels = viewModels.shuffled()
}
}
extension BetaClipsViewController: UICollectionViewDataSource {
func numberOfSections(in collectionView: UICollectionView) -> Int {
return viewModels.count
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return viewModels[section].count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cellType = viewModels[indexPath.section][indexPath.row]
switch cellType {
case .clip(let viewModel):
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: ClipsCollectionViewCell.identifier,
for: indexPath)
as? ClipsCollectionViewCell else {
fatalError()
}
cell.delegate = self
cell.configure(with: viewModel)
return cell
}
}
}
extension BetaClipsViewController: ClipsCollectionViewCellDelegate {
func didTapProfile(with model: VideoModel) {
print("profile tapped")
let owner = model.username
DatabaseManager.shared.findUser(username: owner) { [weak self] user in
DispatchQueue.main.async {
guard let user = user else {
return
}
let vc = ProfileViewController(user: user)
self?.navigationController?.pushViewController(vc, animated: true)
}
}
}
func didTapShare(with model: VideoModel) {
print("tapped share")
}
func didTapNewClip(with model: VideoModel) {
let vc = RecordViewController()
navigationController?.pushViewController(vc, animated: true)
}
}
extension BetaClipsViewController {
func createViewModel(
model: Clip,
username: String,
completion: #escaping (Bool) -> Void
) {
// StorageManager.shared.profilePictureURL(for: username) { [weak self] profilePictureURL in
// guard let clipURL = URL(string: model.clipUrlString),
// let profilePhotoUrl = profilePictureURL else {
// return
// }
let clipData: [ClipFeedCellType] = [
.clip(viewModel: VideoModel(username: username,
videoFileURL: model.clipUrlString))
]
self.viewModels.append(clipData)
completion(true)
// }
}
}
The cell:
import UIKit
import AVFoundation
protocol ClipsCollectionViewCellDelegate: AnyObject {
func didTapProfile(with model: VideoModel)
func didTapShare(with model: VideoModel)
func didTapNewClip(with model: VideoModel)
}
class ClipsCollectionViewCell: UICollectionViewCell {
static let identifier = "ClipsCollectionViewCell"
var playerLooper: NSObject?
// Labels
private let usernameLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.textColor = UIColor.systemPink.withAlphaComponent(0.5)
label.backgroundColor = UIColor.systemPink.withAlphaComponent(0.1)
label.clipsToBounds = true
label.layer.cornerRadius = 8
return label
}()
// Buttons
private let profileButton: UIButton = {
let button = UIButton()
button.setBackgroundImage(UIImage(systemName: "person.circle"), for: .normal)
button.tintColor = .white
button.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.1)
button.clipsToBounds = true
button.layer.cornerRadius = 32
button.isUserInteractionEnabled = true
return button
}()
private let shareButton: UIButton = {
let button = UIButton()
button.setBackgroundImage(UIImage(systemName: "square.and.arrow.down"), for: .normal)
button.tintColor = .white
button.backgroundColor = UIColor.systemBlue.withAlphaComponent(0.1)
button.clipsToBounds = true
button.layer.cornerRadius = 4
button.isUserInteractionEnabled = true
return button
}()
private let newClipButton: UIButton = {
let button = UIButton()
button.setBackgroundImage(UIImage(systemName: "plus"), for: .normal)
button.tintColor = .systemOrange
button.backgroundColor = UIColor.systemOrange.withAlphaComponent(0.1)
button.clipsToBounds = true
button.layer.cornerRadius = 25
button.isUserInteractionEnabled = true
return button
}()
private let videoContainer = UIView()
// Delegate
weak var delegate: ClipsCollectionViewCellDelegate?
// Subviews
var player: AVPlayer?
private var model: VideoModel?
override init(frame: CGRect) {
super.init(frame: frame)
contentView.backgroundColor = .black
contentView.clipsToBounds = true
addSubviews()
}
private func addSubviews() {
contentView.addSubview(videoContainer)
contentView.addSubview(usernameLabel)
contentView.addSubview(profileButton)
contentView.addSubview(shareButton)
contentView.addSubview(newClipButton)
// Add actions
profileButton.addTarget(self, action: #selector(didTapProfileButton), for: .touchUpInside)
shareButton.addTarget(self, action: #selector(didTapShareButton), for: .touchUpInside)
newClipButton.addTarget(self, action: #selector(didTapNewClipButton), for: .touchUpInside)
videoContainer.clipsToBounds = true
contentView.sendSubviewToBack(videoContainer)
}
#objc private func didTapProfileButton() {
guard let model = model else {
return
}
delegate?.didTapProfile(with: model)
}
#objc private func didTapShareButton() {
guard let model = model else {
return
}
delegate?.didTapShare(with: model)
}
#objc private func didTapNewClipButton() {
guard let model = model else {
return
}
delegate?.didTapNewClip(with: model)
}
override func layoutSubviews() {
super.layoutSubviews()
videoContainer.frame = contentView.bounds
let size = contentView.frame.size.width/7
let width = contentView.frame.size.width
let height = contentView.frame.size.height
// Labels
usernameLabel.frame = CGRect(x: (width-(size*3))/2, y: height-880-(size/2), width: size*3, height: size)
// Buttons
profileButton.frame = CGRect(x: width-(size*7), y: height-850-size, width: size, height: size)
shareButton.frame = CGRect(x: width-size, y: height-850-size, width: size, height: size)
newClipButton.frame = CGRect(x: width-size-10, y: height-175-size, width: size/1.25, height: size/1.25)
}
override func prepareForReuse() {
super.prepareForReuse()
usernameLabel.text = nil
player?.pause()
player?.seek(to: CMTime.zero)
}
public func configure(with model: VideoModel) {
self.model = model
configureVideo()
// Labels
usernameLabel.text = "#" + model.username
}
private func configureVideo() {
guard let model = model else {
return
}
guard let url = URL(string: model.videoFileURL) else { return }
player = AVPlayer(url: url)
let playerView = AVPlayerLayer()
playerView.player = player
playerView.frame = contentView.bounds
playerView.videoGravity = .resizeAspectFill
videoContainer.layer.addSublayer(playerView)
player?.volume = 5
player?.play()
player?.actionAtItemEnd = .none
NotificationCenter.default.addObserver(self,
selector: #selector(playerItemDidReachEnd(notification:)),
name: .AVPlayerItemDidPlayToEndTime,
object: player?.currentItem)
}
#objc func playerItemDidReachEnd(notification: Notification) {
if let playerItem = notification.object as? AVPlayerItem {
playerItem.seek(to: .zero, completionHandler: nil)
}
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

My UIView doesn't show up as I try to set constraints relative to safe area programmatically

I have a StackContainerView inside my main view controller called TodayPicksViewController. I am trying to programmatically set the StackContainerView to fill up the whole view controller side to side, with around 50 from top and bottom (just like a Tinder card).
However, as I try to implement constraints relative to safe area as follows(as other answers on StackOverflow suggest), turned out the StackContainerView doesn't show up at all. I don't know where the problem is.
Please advice.
Code of my main view controller, TodayPicksViewController:
class TodayPicksViewController: UIViewController {
//MARK: - Properties
var viewModelData = [CardsDataModel(bgColor: UIColor(red:0.96, green:0.81, blue:0.46, alpha:1.0), text: "Hamburger", image: "hamburger"),
CardsDataModel(bgColor: UIColor(red:0.29, green:0.64, blue:0.96, alpha:1.0), text: "Puppy", image: "puppy"),
CardsDataModel(bgColor: UIColor(red:0.29, green:0.63, blue:0.49, alpha:1.0), text: "Poop", image: "poop"),
CardsDataModel(bgColor: UIColor(red:0.69, green:0.52, blue:0.38, alpha:1.0), text: "Panda", image: "panda"),
CardsDataModel(bgColor: UIColor(red:0.90, green:0.99, blue:0.97, alpha:1.0), text: "Subway", image: "subway"),
CardsDataModel(bgColor: UIColor(red:0.83, green:0.82, blue:0.69, alpha:1.0), text: "Robot", image: "robot")]
var stackContainer : StackContainerView!
private let spinner = JGProgressHUD(style: .dark)
private var users = [[String: String]]()
private var results = [SearchResult]()
private var hasFetched = false
var divisor: CGFloat!
private let noResultsLabel: UILabel = {
let label = UILabel()
label.isHidden = true
label.text = "No Results"
label.textAlignment = .center
label.textColor = .green
label.font = .systemFont(ofSize: 21, weight: .medium)
return label
}()
override func loadView() {
view = UIView()
stackContainer = StackContainerView()
view.addSubview(stackContainer)
stackContainer.translatesAutoresizingMaskIntoConstraints = false
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(noResultsLabel)
configureStackContainer()
stackContainer.dataSource = self
}
#IBAction func panMatch(_ sender: UIPanGestureRecognizer) {
let match = sender.view!
let point = sender.translation(in: view)
let xFromCenter = match.center.x - view.center.x
print(xFromCenter)
match.center = CGPoint(x: view.center.x + point.x, y: view.center.y + point.y)
match.transform = CGAffineTransform(rotationAngle: xFromCenter/divisor)
if sender.state == UIGestureRecognizer.State.ended {
if match.center.x < 75 {
// Move off to the left side
UIView.animate(withDuration: 0.3, animations: {
match.center = CGPoint(x: match.center.x - 200, y: match.center.y + 75)
match.alpha = 0
})
return
} else if match.center.x > (view.frame.width - 75) {
// Move off to the right side
UIView.animate(withDuration: 0.3, animations: {
match.center = CGPoint(x: match.center.x + 200, y: match.center.y + 75)
match.alpha = 0
})
return
}
// resetCard()
}
}
private var loginObserver: NSObjectProtocol?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
validateAuth()
}
private func validateAuth() {
if FirebaseAuth.Auth.auth().currentUser == nil {
let vc = SignInViewController()
let nav = UINavigationController(rootViewController: vc)
nav.modalPresentationStyle = .fullScreen
present(nav, animated: false)
}
}
#objc private func pageControlDidChange(_ sender: UIPageControl) {
let current = sender.currentPage
// scrollView.setContentOffset(CGPoint(x: CGFloat(current) * view.frame.size.width,
// y: 70), animated: true)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
}
//MARK: - Configurations
func configureStackContainer() {
stackContainer.centerXAnchor.constraint(equalTo: self.view.centerXAnchor).isActive = true
stackContainer.centerYAnchor.constraint(equalTo: self.view.centerYAnchor, constant: -60).isActive = true
// stackContainer.widthAnchor.constraint(equalToConstant: 300).isActive = true
// stackContainer.heightAnchor.constraint(equalToConstant: 400).isActive = true
stackContainer.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor).isActive = true
stackContainer.leadingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.leadingAnchor).isActive = true
stackContainer.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor).isActive = true
stackContainer.bottomAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor).isActive = true
}
func updateUI() {
if results.isEmpty {
noResultsLabel.isHidden = false
}
else {
noResultsLabel.isHidden = true
}
}
func calcAge(birthday: Date) -> Int {
let dateFormater = DateFormatter()
dateFormater.dateFormat = "MM/dd/yyyy"
// let birthdayDate = dateFormater.date(from: birthday)
let calendar: NSCalendar! = NSCalendar(calendarIdentifier: .gregorian)
let now = Date()
let calcAge = calendar.components(.year, from: birthday, to: now, options: [])
let age = calcAge.year
return age!
}
extension TodayPicksViewController : SwipeCardsDataSource {
func numberOfCardsToShow() -> Int {
return viewModelData.count
}
func card(at index: Int) -> SwipeCardView {
let card = SwipeCardView()
card.dataSource = viewModelData[index]
return card
}
func emptyView() -> UIView? {
return nil
}
}
Probably doesn't matter, but here is my code for the StackContainerView:
class StackContainerView: UIView, SwipeCardsDelegate {
//MARK: - Properties
var numberOfCardsToShow: Int = 0
var cardsToBeVisible: Int = 3
var cardViews : [SwipeCardView] = []
var remainingcards: Int = 0
let horizontalInset: CGFloat = 10.0
let verticalInset: CGFloat = 10.0
var visibleCards: [SwipeCardView] {
return subviews as? [SwipeCardView] ?? []
}
var dataSource: SwipeCardsDataSource? {
didSet {
reloadData()
}
}
//MARK: - Init
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .clear
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func reloadData() {
removeAllCardViews()
guard let datasource = dataSource else { return }
setNeedsLayout()
layoutIfNeeded()
numberOfCardsToShow = datasource.numberOfCardsToShow()
remainingcards = numberOfCardsToShow
for i in 0..<min(numberOfCardsToShow,cardsToBeVisible) {
addCardView(cardView: datasource.card(at: i), atIndex: i )
}
}
//MARK: - Configurations
private func addCardView(cardView: SwipeCardView, atIndex index: Int) {
cardView.delegate = self
addCardFrame(index: index, cardView: cardView)
cardViews.append(cardView)
insertSubview(cardView, at: 0)
remainingcards -= 1
}
func addCardFrame(index: Int, cardView: SwipeCardView) {
var cardViewFrame = bounds
let horizontalInset = (CGFloat(index) * self.horizontalInset)
let verticalInset = CGFloat(index) * self.verticalInset
cardViewFrame.size.width -= 2 * horizontalInset
cardViewFrame.origin.x += horizontalInset
cardViewFrame.origin.y += verticalInset
cardView.frame = cardViewFrame
}
private func removeAllCardViews() {
for cardView in visibleCards {
cardView.removeFromSuperview()
}
cardViews = []
}
func swipeDidEnd(on view: SwipeCardView) {
guard let datasource = dataSource else { return }
view.removeFromSuperview()
if remainingcards > 0 {
let newIndex = datasource.numberOfCardsToShow() - remainingcards
addCardView(cardView: datasource.card(at: newIndex), atIndex: 2)
for (cardIndex, cardView) in visibleCards.reversed().enumerated() {
UIView.animate(withDuration: 0.2, animations: {
cardView.center = self.center
self.addCardFrame(index: cardIndex, cardView: cardView)
self.layoutIfNeeded()
})
}
}else {
for (cardIndex, cardView) in visibleCards.reversed().enumerated() {
UIView.animate(withDuration: 0.2, animations: {
cardView.center = self.center
self.addCardFrame(index: cardIndex, cardView: cardView)
self.layoutIfNeeded()
})
}
}
}
}
According to the apple developer doc for loadView(), they said "The view controller calls this method when its view property is requested but is currently nil. This method loads or creates a view and assigns it to the view property." This might be the cause of the problem. I would recommend you to perform the view set up operations in viewDidLoad or other proper lifecycle methods. Based on my understanding, this line view = UIView() isn't necessary. In your configureStackContainer() func, you set the centerX and centerY anchor and then set the top, leading, trailing, bottom anchor again. This may also raise the constraint conflicts. I think you don't need to specify centerX and centerY anchor if you want to constraint with top, leading, trailing and bottom and vice versa. I hope this will be helpful.

Not able to remove custom UIView from SuperView

This is extremely odd. I am trying to remove the view from superview when I drag the view to either left or right. If the view doesn't contain any subviews then I am easily able to remove the view from the superView by using this card.removeFromSuperview() - however, what I have noticed is that if add two views as subviews inside the card view then I am not able to remove it from superView and the entire thing goes bezerk.
Here is the card view class:
class MainSwipeCardView: UIView {
//MARK: - Properties
var swipeView = UIView()
var shadowView = UIView()
var text: String?
var label = UILabel()
var bgColor : UIColor? {
didSet {
swipeView.backgroundColor = bgColor
}
}
var cardsarraydata : CardDataModel? {
didSet {
bgColor = cardsarraydata?.backgroundColor
label.text = cardsarraydata?.title
}
}
var delegate : CardDelegate?
//MARK :- Init
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .clear
configureShadowView()
configureSwipeView()
configureLabelView()
addPanGestureOnCards()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Configurations
func configureShadowView() {
shadowView.backgroundColor = .clear
shadowView.layer.shadowColor = UIColor.black.cgColor
shadowView.layer.shadowOffset = CGSize(width: 0, height: 0)
shadowView.layer.shadowOpacity = 0.8
shadowView.layer.shadowRadius = 4.0
addSubview(shadowView)
shadowView.translatesAutoresizingMaskIntoConstraints = false
shadowView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
shadowView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
shadowView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
shadowView.topAnchor.constraint(equalTo: topAnchor).isActive = true
}
func configureSwipeView() {
swipeView.layer.cornerRadius = 15
swipeView.clipsToBounds = true
shadowView.addSubview(swipeView)
swipeView.translatesAutoresizingMaskIntoConstraints = false
swipeView.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
swipeView.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
swipeView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
swipeView.topAnchor.constraint(equalTo: topAnchor).isActive = true
}
func configureLabelView() {
swipeView.addSubview(label)
label.backgroundColor = .white
label.textColor = .black
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 18)
label.translatesAutoresizingMaskIntoConstraints = false
label.leftAnchor.constraint(equalTo: leftAnchor).isActive = true
label.rightAnchor.constraint(equalTo: rightAnchor).isActive = true
label.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true
label.heightAnchor.constraint(equalToConstant: 85).isActive = true
}
func addPanGestureOnCards() {
self.isUserInteractionEnabled = true
addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture)))
}
//MARK: - Handlers
#objc func handlePanGesture(sender: UIPanGestureRecognizer){
let card = sender.view as! MainSwipeCardView
let point = sender.translation(in: self)
let centerOfParentContainer = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2)
card.center = CGPoint(x: centerOfParentContainer.x + point.x, y: centerOfParentContainer.y + point.y)
switch sender.state {
case .ended:
if (card.center.x) > 400 {
delegate?.swipeDidEnd(on: card)
UIView.animate(withDuration: 0.2) {
card.center = CGPoint(x: centerOfParentContainer.x + point.x + 200, y: centerOfParentContainer.y + point.y + 75)
card.alpha = 0
self.layoutIfNeeded()
}
return
}else if card.center.x < -115 {
delegate?.swipeDidEnd(on: card)
UIView.animate(withDuration: 0.2) {
card.center = CGPoint(x: centerOfParentContainer.x + point.x - 200, y: centerOfParentContainer.y + point.y + 75)
card.alpha = 0
self.layoutIfNeeded()
}
return
}
UIView.animate(withDuration: 0.2) {
card.transform = .identity
card.center = CGPoint(x: self.frame.width / 2, y: self.frame.height / 2)
self.layoutIfNeeded()
}
default:
break
}
}
In this subclass I have two UIViews, I am adding the views sequentially. Then on swipeView I am adding the text and label and background color. This is how the cards look like:
I am also using a UIPanInteraction on it and so if I drag it to left or right, then I call the delegate which removes the entire MainSwipeCardView from the container view. In doing so this is what happens:
It keeps adding more and more in the background even though this is what I am calling in the delegate function:
func swipeDidEnd(on card: MainSwipeCardView) {
card.removeFromSuperview()
print(visibleCards.count)
}
The visibleCards is essentially an array of subviews in the container view. It should decrease so for example from 3 -> 2 -> 1; but it increases in non linear way ( not able to really get a relationship out of it)
The most confusing thing is that I am actually able to run this whole code just fine if I donot add the SwipeView and shadowView properties inside the custom view and just use the customView itself to house the label and the backgroundColor. When I add these two properties, then this whole thing seem to go haywire.
Please any kind of help will be extremely appreciated. Thanks!
ContainerView code is as follows:
class SwipeCardContainerView: UIView, CardDelegate {
//MARK: - Properties
var numberOfCards: Int = 0
var remainingCards: Int = 0
var cardsView : [MainSwipeCardView] = []
var numberOfAllowedCard: Int = 3
let horizontalInset: CGFloat = 8.0
let verticalInset: CGFloat = 8.0
var visibleCards : [MainSwipeCardView] {
return subviews as? [MainSwipeCardView] ?? []
}
var datasource : CardDataSource? {
didSet {
loadData()
}
}
override init(frame: CGRect) {
super.init(frame: .zero)
backgroundColor = .clear
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - Configuration
func loadData() {
guard let datasource = datasource else { return }
numberOfCards = datasource.numberOfCards()
remainingCards = numberOfCards
for i in 0..<min(numberOfCards,numberOfAllowedCard) {
addCardView(at: i, card: datasource.createCard(at: i))
}
setNeedsLayout()
}
func addCardView(at index: Int, card: MainSwipeCardView) {
card.delegate = self
addCardFrame(index: index, cardView: card)
cardsView.append(card)
insertSubview(card, at: 0)
remainingCards -= 1
}
func addCardFrame(index: Int, cardView: MainSwipeCardView){
cardsView.append(cardView)
var cardViewFrame = bounds
let horizontalInset = (CGFloat(index) * self.horizontalInset)
let verticalInset = CGFloat(index) * self.verticalInset
cardViewFrame.size.width -= 2 * horizontalInset
cardViewFrame.origin.x += horizontalInset
cardViewFrame.origin.y += verticalInset
cardView.frame = cardViewFrame
}
// Delegate Method
func swipeDidEnd(on card: MainSwipeCardView) {
card.removeFromSuperview()
print(visibleCards.count)
}
Main ViewController Code:
class ViewController: UIViewController {
//MARK: - Properties
var stackContainer : SwipeCardContainerView!
var cardDataArray : [CardDataModel] = [CardDataModel(backgroundColor: .orange, title: "Hello"),
CardDataModel(backgroundColor: .red, title: "Red"),
CardDataModel(backgroundColor: .blue, title: "Blue"),
CardDataModel(backgroundColor: .orange, title: "Orange")]
//MARK: - Init
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red:0.93, green:0.93, blue:0.93, alpha:1.0)
stackContainer = SwipeCardContainerView()
view.addSubview(stackContainer)
configureSwipeContainerView()
stackContainer.translatesAutoresizingMaskIntoConstraints = false
}
//MARK : - Configurations
func configureSwipeContainerView() {
stackContainer.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
stackContainer.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -50).isActive = true
stackContainer.widthAnchor.constraint(equalToConstant: 300).isActive = true
stackContainer.heightAnchor.constraint(equalToConstant: 350).isActive = true
}
override func viewDidLayoutSubviews() {
stackContainer.datasource = self
}
//MARK : - Handlers
}
extension ViewController : CardDataSource {
func numberOfCards() -> Int {
return cardDataArray.count
}
func createCard(at index: Int) -> MainSwipeCardView {
let card = MainSwipeCardView()
card.cardsarraydata = cardDataArray[index]
return card
}
func emptyCard() -> UIView? {
return nil
}
}
I've investigated the problem.
First issue is in the ViewController:
override func viewDidLayoutSubviews() {
stackContainer.datasource = self
}
Just remove this code. In each layout you set datasource... and loadData... this is incorrect approach, also super.viewDidLayoutSubviews() is missing...
And also stackContainer.datasource = self:
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor(red:0.93, green:0.93, blue:0.93, alpha:1.0)
stackContainer = SwipeCardContainerView()
view.addSubview(stackContainer)
configureSwipeContainerView()
stackContainer.translatesAutoresizingMaskIntoConstraints = false
stackContainer.datasource = self
Second issue is in func loadData(), just replace it with
func loadData() {
guard let datasource = datasource else { return }
setNeedsLayout()
layoutIfNeeded()
numberOfCards = datasource.numberOfCards()
remainingCards = numberOfCards
for i in 0..<min(numberOfCards,numberOfAllowedCard) {
addCardView(at: i, card: datasource.createCard(at: i))
}
}
or find better solution with layout of SwipeCardContainerView

Forwarding events with hitTest() or point(inside, with event) from view added to keyWindow

I've got a small, reusable UIView widget that can be added to any view anywhere, and may or may not always be in the same place or have the same frame. It looks something like this:
class WidgetView: UIView {
// some stuff, not very exciting
}
In my widget view, there's a situation where I need to create a popup menu with an overlay underneath it. it looks like this:
class WidgetView: UIView {
// some stuff, not very exciting
var overlay: UIView!
commonInit() {
guard let keyWindow = UIApplication.shared.keyWindow else { return }
overlay = UIView(frame: keyWindow.frame)
overlay.alpha = 0
keyWindow.addSubview(overlay)
// Set some constraints here
someControls = CustomControlsView( ... a smaller controls view ... )
overlay.addSubview(someControls)
// Set some more constraints here!
}
showOverlay() {
overlay.alpha = 1
}
hideOverlay() {
overlay.alpha = 0
}
}
Where this gets complicated, is I'm cutting the shape of the originating WidgetView out of the overlay, so that its controls are still visible underneath. This works fine:
class CutoutView: UIView {
var holes: [CGRect]?
convenience init(holes: [CGRect], backgroundColor: UIColor?) {
self.init()
self.holes = holes
self.backgroundColor = backgroundColor ?? UIColor.black.withAlphaComponent(0.5)
isOpaque = false
}
override func draw(_ rect: CGRect) {
backgroundColor?.setFill()
UIRectFill(rect)
guard let rectsArray = holes else {
return
}
for holeRect in rectsArray {
let holeRectIntersection = rect.intersection(holeRect)
UIColor.clear.setFill()
UIRectFill(holeRectIntersection)
}
}
}
... except the problem:
Touches aren't forwarded through the cutout hole. So I thought I'd be clever, and use this extension to determine whether the pixels at the touch point are transparent or not, but I can't even get that far, because hitTest() and point(inside, with event) don't respond to touches outside of the WidgetView's frame.
The way I can see it, there are four (potential) ways to solve this, but I can't get any of them working.
Find some magical (🦄) way to to make hitTest or point(inside) respond anywhere in the keyWindow, or at least the overlayView's frame
Add a UITapGestureRecognizer to the overlayView and forward the appropriate touches to the originating view controller (this partially works — the tap gesture responds, but I don't know where to go from there)
Use a delegate/protocol implementation to tell the original WidgetView to respond to touches
Add the overlay and its subviews to a different parent view altogether that isn't the keyWindow?
Below the fold, here is a complete executable setup, which relies on a new single view project with storyboard. It relies on SnapKit constraints, for which you can use the following podfile:
podfile
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!
target 'YourTarget' do
pod 'SnapKit', '~> 4.2.0'
end
ViewController.swift
import UIKit
import SnapKit
class ViewController: UIViewController {
public var utilityToolbar: UtilityToolbar!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .darkGray
setup()
}
func setup() {
let button1 = UtilityToolbar.Button(title: "One", buttonPressed: nil)
let button2 = UtilityToolbar.Button(title: "Two", buttonPressed: nil)
let button3 = UtilityToolbar.Button(title: "Three", buttonPressed: nil)
let button4 = UtilityToolbar.Button(title: "Four", buttonPressed: nil)
let button5 = UtilityToolbar.Button(title: "Five", buttonPressed: nil)
let menuItems: [UtilityToolbar.Button] = [button1, button2, button3, button4, button5]
menuItems.forEach({
$0.setTitleColor(#colorLiteral(red: 0.1963312924, green: 0.2092989385, blue: 0.2291107476, alpha: 1), for: .normal)
})
utilityToolbar = UtilityToolbar(title: "One", menuItems: menuItems)
utilityToolbar.titleButton.setTitleColor(#colorLiteral(red: 0.1963312924, green: 0.2092989385, blue: 0.2291107476, alpha: 1), for: .normal)
utilityToolbar.backgroundColor = .white
utilityToolbar.dropdownContainer.backgroundColor = .white
view.addSubview(utilityToolbar)
utilityToolbar.snp.makeConstraints { (make) in
make.left.right.equalToSuperview()
make.top.equalToSuperview().offset(250)
make.height.equalTo(50.0)
}
}
}
CutoutView.swift
import UIKit
class CutoutView: UIView {
var holes: [CGRect]?
convenience init(holes: [CGRect], backgroundColor: UIColor?) {
self.init()
self.holes = holes
self.backgroundColor = backgroundColor ?? UIColor.black.withAlphaComponent(0.5)
isOpaque = false
}
override func draw(_ rect: CGRect) {
backgroundColor?.setFill()
UIRectFill(rect)
guard let rectsArray = holes else { return }
for holeRect in rectsArray {
let holeRectIntersection = rect.intersection(holeRect)
UIColor.clear.setFill()
UIRectFill(holeRectIntersection)
}
}
}
UtilityToolbar.swift
import Foundation import UIKit import SnapKit
class UtilityToolbar: UIView {
class Button: UIButton {
var functionIdentifier: String?
var buttonPressed: (() -> Void)?
fileprivate var owner: UtilityToolbar?
convenience init(title: String, buttonPressed: (() -> Void)?) {
self.init(type: .custom)
self.setTitle(title, for: .normal)
self.functionIdentifier = title.lowercased()
self.buttonPressed = buttonPressed
}
}
enum MenuState {
case open
case closed
}
enum TitleStyle {
case label
case dropdown
}
private(set) public var menuState: MenuState = .closed
var itemHeight: CGFloat = 50.0
var spacing: CGFloat = 6.0 { didSet { dropdownStackView.spacing = spacing } }
var duration: TimeInterval = 0.15
var dropdownContainer: UIView!
var titleButton: UIButton = UIButton()
#IBOutlet weak fileprivate var toolbarStackView: UIStackView!
private var stackViewBottomConstraint: Constraint!
private var dropdownStackView: UIStackView!
private var overlayView: CutoutView!
private var menuItems: [Button] = []
private var expandedHeight: CGFloat { get { return CGFloat(menuItems.count - 1) * itemHeight + (spacing * 2) } }
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
convenience init(title: String, menuItems: [Button]) {
self.init()
self.titleButton.setTitle(title, for: .normal)
self.menuItems = menuItems
commonInit()
}
private func commonInit() {
self.addSubview(titleButton)
titleButton.addTarget(self, action: #selector(titleButtonPressed(_:)), for: .touchUpInside)
titleButton.snp.makeConstraints { $0.edges.equalToSuperview() }
dropdownContainer = UIView()
dropdownStackView = UIStackView()
dropdownStackView.axis = .vertical
dropdownStackView.distribution = .fillEqually
dropdownStackView.alignment = .fill
dropdownStackView.spacing = spacing
dropdownStackView.alpha = 0
dropdownStackView.translatesAutoresizingMaskIntoConstraints = true
menuItems.forEach({
$0.owner = self
$0.addTarget(self, action: #selector(menuButtonPressed(_:)), for: .touchUpInside)
})
}
override func layoutSubviews() {
super.layoutSubviews()
// Block if the view isn't fully ready, or if the containerView has already been added to the window
guard
let keyWindow = UIApplication.shared.keyWindow,
self.globalFrame != .zero,
dropdownContainer.superview == nil else { return }
overlayView = CutoutView(frame: keyWindow.frame)
overlayView.backgroundColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0.5)
overlayView.alpha = 0
overlayView.holes = [self.globalFrame!]
keyWindow.addSubview(overlayView)
keyWindow.addSubview(dropdownContainer)
dropdownContainer.snp.makeConstraints { (make) in
make.left.right.equalToSuperview()
make.top.equalToSuperview().offset((self.globalFrame?.origin.y ?? 0) + self.frame.height)
make.height.equalTo(0)
}
dropdownContainer.addSubview(dropdownStackView)
dropdownStackView.snp.makeConstraints({ (make) in
make.left.right.equalToSuperview().inset(spacing).priority(.required)
make.top.equalToSuperview().priority(.medium)
stackViewBottomConstraint = make.bottom.equalToSuperview().priority(.medium).constraint
})
}
public func openMenu() {
titleButton.isSelected = true
dropdownStackView.addArrangedSubviews(menuItems.filter { $0.titleLabel?.text != titleButton.titleLabel?.text })
dropdownContainer.layoutIfNeeded()
dropdownContainer.snp.updateConstraints { (make) in
make.height.equalTo(self.expandedHeight)
}
stackViewBottomConstraint.update(inset: spacing)
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .curveEaseOut, animations: {
self.overlayView.alpha = 1
self.dropdownStackView.alpha = 1
self.dropdownContainer.superview?.layoutIfNeeded()
}) { (done) in
self.menuState = .open
}
}
public func closeMenu() {
titleButton.isSelected = false
dropdownContainer.snp.updateConstraints { (make) in
make.height.equalTo(0)
}
stackViewBottomConstraint.update(inset: 0)
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0, options: .curveEaseOut, animations: {
self.overlayView.alpha = 0
self.dropdownStackView.alpha = 0
self.dropdownContainer.superview?.layoutIfNeeded()
}) { (done) in
self.menuState = .closed
self.dropdownStackView.removeAllArrangedSubviews()
}
}
#objc private func titleButtonPressed(_ sender: Button) {
switch menuState {
case .open:
closeMenu()
case .closed:
openMenu()
}
}
#objc private func menuButtonPressed(_ sender: Button) {
closeMenu()
}
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
// Nothing of interest is happening here unless the touch is inside the containerView
print(UIColor.colorOfPoint(point: point, in: overlayView).cgColor.alpha > 0)
if UIColor.colorOfPoint(point: point, in: overlayView).cgColor.alpha > 0 {
return true
}
return super.point(inside: point, with: event)
} }
Extensions.swift
import UIKit
extension UIWindow {
static var topController: UIViewController? {
get {
guard var topController = UIApplication.shared.keyWindow?.rootViewController else { return nil }
while let presentedViewController = topController.presentedViewController {
topController = presentedViewController
}
return topController
}
}
}
public extension UIView {
var globalPoint: CGPoint? {
return self.superview?.convert(self.frame.origin, to: nil)
}
var globalFrame: CGRect? {
return self.superview?.convert(self.frame, to: nil)
}
}
extension UIColor {
static func colorOfPoint(point:CGPoint, in view: UIView) -> UIColor {
var pixel: [CUnsignedChar] = [0, 0, 0, 0]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
let context = CGContext(data: &pixel, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)
context!.translateBy(x: -point.x, y: -point.y)
view.layer.render(in: context!)
let red: CGFloat = CGFloat(pixel[0]) / 255.0
let green: CGFloat = CGFloat(pixel[1]) / 255.0
let blue: CGFloat = CGFloat(pixel[2]) / 255.0
let alpha: CGFloat = CGFloat(pixel[3]) / 255.0
let color = UIColor(red:red, green: green, blue:blue, alpha:alpha)
return color
}
}
extension UIStackView {
func addArrangedSubviews(_ views: [UIView?]) {
views.filter({$0 != nil}).forEach({ self.addArrangedSubview($0!)})
}
func removeAllArrangedSubviews() {
let removedSubviews = arrangedSubviews.reduce([]) { (allSubviews, subview) -> [UIView] in
self.removeArrangedSubview(subview)
return allSubviews + [subview]
}
// Deactivate all constraints
NSLayoutConstraint.deactivate(removedSubviews.flatMap({ $0.constraints }))
// Remove the views from self
removedSubviews.forEach({ $0.removeFromSuperview() })
}
}
Silly me, I need to put the hitTest on the overlay view (CutoutView) not the calling view.
class CutoutView: UIView {
// ...
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard UIColor.colorOfPoint(point: point, in: self).cgColor.alpha > 0 else { return nil }
return super.hitTest(point, with: event)
}
}

ios-charts: Axes and gridlines are intermittently missing

I'm using iOS-charts by danielGindi to display a line chart. The desired behavior is to show the left-axis, x-axis and horizontal gridlines, however, sometimes one or both of the axes is missing and/or one or more of the gridlines is missing.
My code is below.
class LineChartViewController: UIViewController {
let timeIntervalCount = 15
#IBOutlet weak var lineChartView: LineChartView!
override func viewDidLoad() {
super.viewDidLoad()
configureChart()
setChartValues()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func randomizeTapped(_ sender: UIButton) {
let count = Int(arc4random_uniform(20) + 3)
setChartValues(count)
}
#IBAction func configureTapped(_ sender: UIButton) {
configureChart()
lineChartView.setNeedsDisplay()
}
#IBAction func closeTapped(_ sender: UIButton) {
dismiss (animated: true, completion: nil)
}
fileprivate func configureChart () {
lineChartView.xAxis.labelPosition = .bottom
lineChartView.xAxis.drawGridLinesEnabled = false
lineChartView.xAxis.avoidFirstLastClippingEnabled = true
lineChartView.xAxis.axisLineColor = UIColor.lightGray
//lineChartView.xAxis.setLabelCount(timeIntervalCount, force: true)
lineChartView.xAxis.avoidFirstLastClippingEnabled = true
lineChartView.leftAxis.enabled = true
lineChartView.leftAxis.axisLineColor = UIColor.lightGray
lineChartView.leftAxis.drawAxisLineEnabled = true
lineChartView.leftAxis.drawGridLinesEnabled = true
lineChartView.leftAxis.gridColor = UIColor.lightGray
lineChartView.leftAxis.axisMinimum = 0
lineChartView.leftAxis.valueFormatter = self
lineChartView.rightAxis.enabled = false
lineChartView.legend.enabled = false
lineChartView.chartDescription?.enabled = false
}
fileprivate func setChartValues (_ count: Int = 15) {
let top = 23
let bottom = 8
let values = (bottom..<top).map { (i) -> ChartDataEntry in
let val = Double(arc4random_uniform(UInt32(count)) + 3)
return ChartDataEntry(x: Double(i), y: val)
}
let set1 = LineChartDataSet(values: values, label: "DataSet 1")
set1.colors = [UIColor.orange]
set1.drawValuesEnabled = false
set1.drawCirclesEnabled = false
let data = LineChartData(dataSet: set1)
self.lineChartView.data = data
}
}
extension LineChartViewController: IAxisValueFormatter {
func stringForValue(_ value: Double,
axis: AxisBase?) -> String {
return String(format: "%.0f", value) + "%"
}
}

Resources