Why won't the viewport move to the last X value of my line chart? - swift

I'm using version 3.1.1. of the Charts framework and I'm having trouble getting the viewport to move far enough to the right to show the last value. If I segue to my ChartViewControllerand the current selection has data to display, it moves to a point in the chart that leaves 7 datapoints out of view to the right. However, if I then select another item that doesn't have any data and then select the previous item that has data, it correctly moves the chart so the last datapoint is visible:
When a selection is made, I fetch the appropriate data:
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
let selectedLift = lifts[row]
UserDefaults.setSelectedLiftForChart(selectedLift)
delegate.fetchLiftEvents()
}
In fetchLiftEvents() I set the chartView.data to nil if there's no data and this appears to be the only difference between what happens when I first segue to the ChartViewController and when select an item with no data and reselect the previous item:
func fetchLiftEvents() {
let liftName = UserDefaults.selectedLiftForChart()
let liftEvents = dataManager.fetchLiftsEventsOfTypeByName(liftName)
guard liftEvents.count > 0 else {
chartView.noDataText = "There's no \(liftName) data to display"
chartView.data = nil
return }
// put them into a Dictionary grouped by each unique day
let groupedEvents = Dictionary(grouping: liftEvents, by: { floor($0.date.timeIntervalSince1970 / 86400) })
// grab the maximum 1RM from each day
let dailyMaximums = groupedEvents.map { $1.max(by: { $0.oneRepMax < $1.oneRepMax }) }
// MARK: - TODO: Fix the silly unwrapping
sortedLiftEvents = dailyMaximums.sorted(by: { $0?.date.compare(($1?.date)!) == .orderedAscending }) as! [LiftEvent]
generatexAxisDates(liftEvents: sortedLiftEvents)
generateLineData(liftEvents: sortedLiftEvents)
}
Both generatexAxisDates(liftEvents: sortedLiftEvents) and generateLineData(liftEvents: sortedLiftEvents) are called in viewDidLoad(), my point being that they're called when the view loads and when a new selection with data is chosen. I'm calling setVisibleXRange(minXRange:maxXRange:) in generateLineData(liftEvents:) so it's always getting called as well:
func generateLineData(liftEvents: [LiftEvent]) {
// generate the array of ChartDataEntry()...
// generate set = LineChartDataSet(values: values, label: "Line DataSet")
let data = LineChartData(dataSet: set)
chartView.data = data
chartView.setVisibleXRange(minXRange: 7.0, maxXRange: 7.0)
chartView.moveViewToX(chartView.chartXMax)
resetxAxis()
chartView.configureDefaults()
chartView.animate(yAxisDuration: 0.8)
}
and resetxAxis() is only doing this:
func resetxAxis() {
let xAxis = chartView.xAxis
xAxis.labelPosition = .bottom
xAxis.axisMinimum = 0
xAxis.granularity = 1
xAxis.axisLineWidth = 5
xAxis.valueFormatter = self
}
I've searched through the documentation and Issues on Github and looked at many related questions on SO, but I'm not able to get this to work. This question seems to be addressing the very same problem I'm having but after trying to apply the solution to my problem, it still persists.
Is there anything else someone can suggest trying?
Update
I found a possible clue to the problem. When I first segue to the chart view (screenshot 1), the bounds of chartView are different after I select an item with no data and then reselect Deadlift (screenshots 2 & 3):
It's the very same data set in each case but initially the height is 389 but then changes to 433. I'm guessing this must affect the scaling but I haven't been able to find where or how the bounds of chartView are set.
Any ideas why this is happening?

I finally figured it out. I was fetching the data that would be displayed on the charts before setting some of the chart properties that should be set prior to setting chartView.data = data. I now set these default properties before fetching and setting the data and it works:
extension BarLineChartViewBase {
func configureDefaults() {
backgroundColor = NSUIColor(red: 35/255.0, green: 43/255.0, blue: 53/255.0, alpha: 1.0)
chartDescription?.enabled = false
legend.enabled = false
xAxis.labelPosition = .bottom
xAxis.avoidFirstLastClippingEnabled = false
xAxis.gridColor.withAlphaComponent(0.9)
rightAxis.enabled = false // this fixed the extra xAxis grid lines
rightAxis.drawLabelsEnabled = false
for axis in [xAxis, leftAxis] as [AxisBase] {
axis.drawAxisLineEnabled = true
axis.labelTextColor = .white
}
noDataTextColor = .white
noDataFont = NSUIFont(name: "HelveticaNeue", size: 16.0)
}

Related

Add 'PageSetupAccessory' to PrintPanel for PDFDocument

I have an app which displays a PDFView, and I want it to print the PDF file from the view. Also, I want the Print panel to show the Page Setup Accessory (i.e. the Paper Size, Orientation and Scale settings, like in Preview, which appear as one panel of the drop-down list of options).
Currently, I'm mistakenly printing the PDFView, not the PDF document itself. This only gives me one page and includes the scrollbars in the print-out! I can't see how to init an NSPrintOperation referencing a PDFDocument rather than the PDFView.
Here's my code, which works, but isn't what I want. I presume I'll have to override either the printDocument or printOperation functions of NSDocument with similar code that defines the Panel and the Info.
func thePrintInfo() -> NSPrintInfo {
let thePrintInfo = NSPrintInfo()
thePrintInfo.horizontalPagination = .automatic // Tried fit
thePrintInfo.verticalPagination = .automatic // Tried fit
thePrintInfo.isHorizontallyCentered = true // Tried false
thePrintInfo.isVerticallyCentered = true // Tried false
thePrintInfo.leftMargin = 0.0
thePrintInfo.rightMargin = 0.0
thePrintInfo.topMargin = 0.0
thePrintInfo.bottomMargin = 0.0
thePrintInfo.jobDisposition = .spool
return thePrintInfo
}
// Need to show the 'Page Setup' Options as an Accessory
// e.g. Paper size, orientation.
#IBAction func printContent(_ sender: Any) {
let printOperation = NSPrintOperation(view: thePDFView, printInfo: thePrintInfo())
let printPanel = NSPrintPanel()
printPanel.options = [
NSPrintPanel.Options.showsCopies,
NSPrintPanel.Options.showsPrintSelection,
NSPrintPanel.Options.showsPageSetupAccessory,
NSPrintPanel.Options.showsPreview
]
printOperation.printPanel = printPanel
printOperation.run()
}
Based on #Willeke's comments, I've come up with the following, which seems to work well. (Minor quibble is that the Print dialog isn't a sheet.) If anyone has any improvements, please post a better answer.
#IBAction func printContent(_ sender: Any) {
let printOperation = thePDFView.document?.printOperation(for: thePrintInfo(), scalingMode: .pageScaleNone, autoRotate: true)
printOperation?.printPanel = thePrintPanel()
printOperation?.run()
}

How to display a set number of rows in small Table View with self-sizing cells

I have a custom table view class, which is configured to set the table view height to display only 3 rows. This means if there are 20 rows, table view will be sized to display first 3 rows, and allowing user to scroll.
This code of mine works only if I set the static rowHeight
class CannedRepliesTableView: UITableView {
/// The max visible rows visible in the autocomplete table before the user has to scroll throught them
let maxVisibleRows = 3
open override var intrinsicContentSize: CGSize {
let rows = numberOfRows(inSection: 0) < maxVisibleRows ? numberOfRows(inSection: 0) : maxVisibleRows
return CGSize(width: super.intrinsicContentSize.width, height: (CGFloat(rows) * rowHeight))
}
}
If I set UITableViewAutomaticDimension to the rowHeight, table view is not properly resized. Is there a solution to this?
This would be one way to improve on what you currently have. I don't have experience accessing intrinsicContentSize for this calculations and didn't test this locally (other than syntax), but if previously it worked, this should as well.
Basically you're creating an array with maxVisibleRows number of indexPaths. If you have less, then fetchedIndexesCount prevents an indexOutOfBounds crash. Once you have the array, you iterate for each corresponding cell and fetching its size, finally summing it up.
class CannedRepliesTableView: UITableView {
var focussedSection = 0
let maxVisibleRows = 3
open override var intrinsicContentSize: CGSize {
return CGSize(width: super.intrinsicContentSize.width, height: calculateHeight())
}
private func calculateHeight() -> CGFloat {
guard let indexPaths = startIndexes(firstCount: 3) else {
// Your table view doesn't have any rows. Feel free to return a non optional and remove this check if confident there's always at least a row
return 0
}
return indexPaths.compactMap({ cellForRow(at: $0)?.intrinsicContentSize.height }).reduce(0, +)
}
private func startIndexes(firstCount x: Int) -> [IndexPath]? {
let rowsCount = numberOfRows(inSection: focussedSection)
let fetchedIndexesCount = min(x, rowsCount)
guard fetchedIndexesCount > 0 else {
return nil
}
var result = [IndexPath]()
for i in 0..<fetchedIndexesCount {
result.append(IndexPath(row: i, section: focussedSection))
}
return result
}
}

How to tap on a button inside a CollectionView cell which is not visible on screen

I am working on UITests using XCode. I have multiple CollectionView cells.
When I perform Count in the collectionView it shows the certain count.
I can able to access first two cells but coming to the 3rd cell as 3(depends on device size). It says that specific button I am looking for in 3rd Cell as exists.
But isHittable is false.
Is there any way I can tap on the button on the 3rd Cell.
I have tried using the extension for forceTapElement() which is available online, it didn’t help.
Extension Used:
extension XCUIElement{
func forceTapElement(){
if self.isHittable{
self.tap()
}else{
let coordinate: XCUICoordinate = self.coordinate(withNormalizedOffset: .zero)
coordinate.tap()
}
}
}
Tried to perform swipeUp() and access the button. it still shows isHittable as false
The only way I've found is to swipe up untile the isHittable will be true.
app.collectionViews.cells.staticTexts["TEST"].tap()
Thread.sleep(forTimeInterval: 3)
let collectionView = app.otherElements.collectionViews.element(boundBy: 0)
let testAds = collectionView.cells
let numberOfTestAds = testAds.count
if numberOfTestAds > 0 {
let tester = collectionView.cells.element(boundBy: 2).buttons["ABC"]
for _ in 0..<100 {
guard !tester.isHittable else {
break;
}
collectionView.swipeUp()
}
}
Please note that the swipeUp() method will only move few pixels. If you want to use more comprehensive methods you can get AutoMate library and try swipe(to:untilVisible:times:avoid:from:):
app.collectionViews.cells.staticTexts["TEST"].tap()
Thread.sleep(forTimeInterval: 3)
let collectionView = app.otherElements.collectionViews.element(boundBy: 0)
let testAds = collectionView.cells
let numberOfTestAds = testAds.count
if numberOfTestAds > 0 {
let tester = collectionView.cells.element(boundBy: 2).buttons["ABC"]
collectionView.swipe(to: .down, untilVisible: tester)
// or swipe max 100 times in case 10 times is not enough
// collectionView.swipe(to: .down, untilVisible: tester, times: 100)
}

UIView not displaying

I am trying to create a simple UIView by calling the following function:
func loadCell() {
let cell : UIView!
cell = UIView(frame: CGRect(x:50, y:50, width:50, height:50))
cell.backgroundColor = UIColor(red:1.00, green:0.00, blue:0.00, alpha:1.00)
self.view?.addSubview(cell)
print("Test")
}
I do not get any errors, however the UIView doesn't seem to appear. The text "Test" is printed to the console which tells me it's not a function calling problem. Another thing to note is that I use the exact same code in a different function at a different part of my program to create a different UIView and that code works perfectly. I have absolutely no idea why this code isn't working. At first I thought it may be a zPosition issue, but after experimenting with the zPosition I see no difference.
I then thought it may have something to do with there already being other UIViews being displayed at the time of calling the function, and the new one that the function "should" be creating isn't getting displayed because the program is thinking "It's ok, you already have a UIView here sir! No need to display another!", as strange as that may be.
Update:
I am calling the loadCell function inside of another function called drawGrid(). drawGrid() is simple a nested for loop which creates a grid of UIViews x along and y high. The drawGrid() function works perfectly when spawning in level 1, however once level 1 is complete and I try to spawn in level 2 that is when it doesn't spawn the next set of UIViews. It prints all of my print("Test")s to the console multiple times which tells me it IS getting to that point in the program but doesn't want to create the next grid of UIViews.
Update 2:
var gridSizeX:Int = 10
var gridSizeY:Int = 10
var gridArray: [[UIView?]] = Array(repeating: Array(repeating: nil, count: gridSizeY), count: gridSizeX)
func loadCell(xPos:Int, yPos:Int) {
let xStart = Int(size.width/2/2) - ((gridSizeX * gridScale)/2)
let yStart = Int(size.height/2/2) - ((gridSizeY * gridScale)/2)
let cell : UIView!
cell = UIView(frame: CGRect(x: xStart + (xPos * gridScale), y:yStart + (yPos * gridScale), width:gridScale, height:gridScale))
cell.layer.borderWidth = 1
cell.layer.borderColor = UIColor(red:0.00, green:0.00, blue:0.00, alpha:0.02).cgColor
cell.backgroundColor = UIColor(red:1.00, green:1.00, blue:1.00, alpha:0)
self.view?.addSubview(cell)
gridArray[xPos][yPos] = cell
}
func drawGrid() {
for y in 0 ... gridSizeY {
for x in 0 ... gridSizeX {
loadCell(xPos:x, yPos:y)
}
}
}
func breakGrid() {
for y in 0 ... gridSizeY - 1 {
for x in 0 ... gridSizeX - 1 {
gridArray[x][y]?.removeFromSuperview()
}
}
}
As you can see, the functions I posted here at the beginning are a little different as I removed unnecessary, irrelevant code not to get confused by. I have added it all back here, and if there is anything you are unsure about just ask :) I also added the "BreakGrid()" function with deletes/removes the previous grid of UIViews. and after deleted I run the drawGrid() again for the next level.
You are most likely calling your method too early, before your view is initialized (probably calling from viewDidLoad?). This would explain why print(self.view) is nil
Try calling your method for viewWillAppear
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadCell()
}
When I do print(self.view) it just prints nil
That means that your self.view is nil so nothing subviews your cell. You should check why your view is nil. Probably it has not been added to self by the time loadCell() is called.
So focus on why the view is nil.
I have checked your code it's work perfect. May your view not updated properly. Can you try this on main thread.
DispatchQueue.main.async {
let cell : UIView!
cell = UIView(frame: CGRect(x:50, y:50, width:50, height:50))
cell.backgroundColor = UIColor(red:1.00, green:0.00, blue:0.00, alpha:1.00)
self.view.addSubview(cell)
print("Test")
}
Please try this
func loadCell() {
let cell : UIView = UIView(frame: CGRect(x:50, y:50, width:50, height:50))
cell.backgroundColor = UIColor(red:1.00, green:0.00, blue:0.00, alpha:1.00)
self.view?.addSubview(cell)
print("Test")
print(self.view)
}
It may be work for you
I have given the size at the time of initialization.

Quickest way to check multiple bools and changing image depending on that bool

I have multiple achievements that the user can achieve during the game. When the user goes to the achievement VC, he should see an image when the achievement is accomplished.
I am now using this code in the viewDidLoad function:
if unlockedAchievement1 == true
{
achievement1Text.textColor = UIColor.greenColor()
achievement1Image.image = UIImage(named: "achievement1Unlocked")
}
if unlockedAchievement2 == true
{
achievement2Text.textColor = UIColor.greenColor()
achievement2Image.image = UIImage(named: "achievement2Unlocked")
}
This would work, but it takes a lot of time to copy paste this overtime. How can I shorten this? Should I make a class? I read about for in loops, but I did not quite understood that. Any help is welcome!
I feel like I have seen this before. But anyway...
//use array of bools. First declare as empty array.
//Use Core data to achieve saving the Booleans if you need to.
let achievements:[Bool] = []
let achievementsImage:[UIImage] = []
let achievementsText:[UITextView] = []
func viewDidLoad() {
//Say you have 5 achievements.
for index in 0 ..< 5 {
achievements.append(false)
achievementsImage.append(UIImage(named: "achievementLocked"))
achievementText.append(UITextView())
//Then you can edit it, such as.
achievementText[index].frame = CGRect(0, 0, 100, 100)
achievementText[index].text = "AwesomeSauce"
self.view.addSubview(achievementText[index])
achievementText[index].textColor = UIColor.blackColor()
}
}
//Call this function and it will run through your achievements and determine what needs to be changed
func someFunction() {
for index in 0 ..< achievements.count {
if(achievements[index]) {
achievements[index] = true
achievementsImage[index] = UIImage(named: String(format: "achievement%iUnlocked", index))
achievementsText[index].textColor = UIColor.greenColor
}
}
}
I'd recommend making your unlockedAchievement1, unlockedAchievement2, etc. objects tuples, with the first value being the original Bool, and the second value being the image name. You'd use this code:
// First create your data array
var achievements = [(Bool, String)]()
// Then you can make your achievements.
// Initially, they're all unearned, so the Bool is false.
let achievement1 = (false, "achievement1UnlockedImageName")
achievements.append(achievement1)
let achievement2 = (false, "achievement2UnlockedImageName")
achievements.append(achievement2)
// Now that you have your finished data array, you can use a simple for-loop to iterate.
// Create a new array to hold the images you'll want to show.
var imagesToShow = [String]()
for achievement in achievements {
if achievement.0 == true {
imagesToShow.append(achievement.1)
}
}