UIDynamicAnimator interfering with addTarget and beginTouches? - ios

I wish I had more reputation so I could attach a screenshot of what I'm working on but you can see an example in the second link down below.
Basically there is a middle button in the tab bar which brings out option buttons (apple images in this case).
These apple icons are animated to their positions using UIDynamicAnimator and UIAttachmentBehavior.
Here is the code which adds the option buttons
func showOptions() {
var numberOfItems = self.delegate.brOptionsButtonNumberOfItems(self)
//NSAssert(numberOfItems > 0 , "number of items should be more than 0")
var angle = 0.0
var radius:Int = 20 * numberOfItems
angle = (180.0 / Double(numberOfItems))
// convert to radians
angle = angle/180.0 * M_PI
for(var i = 0; i<numberOfItems; i++) {
var csCalc = Float((angle * Double(i)) + (angle/2))
var buttonX = Float(radius) * cosf(csCalc)
var buttonY = Float(radius) * sinf(csCalc)
var wut = (angle * Double(i)) + (angle/2)
var brOptionItem = self.createButtonItemAtIndex(i)
var mypoint = self.tabBar.convertPoint(self.center, fromView:self.superview)
var x = mypoint.x + CGFloat(buttonX)
var y = self.frame.origin.y - CGFloat(buttonY)
var buttonPoint = CGPointMake(x, y)
//println("Button Point of Button Item x:\(x) y: \(y)")
brOptionItem.layer.anchorPoint = self.layer.anchorPoint
brOptionItem.center = mypoint
var attachment = UIAttachmentBehavior(item:brOptionItem, attachedToAnchor:buttonPoint)
attachment.damping = self.damping
attachment.frequency = self.frequency
attachment.length = 1
// set the attachment for dragging behavior
brOptionItem.attachment = attachment
self.dynamicItem!.addItem(brOptionItem)
//if(self.delegate.respondsToSelector("willDisplayButtonItem")) { //Fix me
//self.delegate.brOptionsButton(self, willDisplayButtonItem:brOptionItem)
//}
self.tabBar.insertSubview(brOptionItem, belowSubview: self.tabBar)
self.dynamicsAnimator!.addBehavior(attachment)
self.items.addObject(brOptionItem)
}
}
func createButtonItemAtIndex(indexz:NSInteger) -> BROptionsItem {
println("Create button item at index")
var brOptionItem = BROptionsItem(initWithIndex:indexz)
brOptionItem.addTarget(self, action:"buttonItemPressed:", forControlEvents:.TouchUpInside)
brOptionItem.autoresizingMask = UIViewAutoresizing.None
var image = self.delegate.brOptionsButton(self, imageForItemAtIndex:indexz)
//if((image) != nil) {
brOptionItem.setImage(image, forState: UIControlState.Normal)
//}
/*
var buttonTitle = self.delegate.brOptionsButton(self, titleForItemAtIndex:indexz)
if(buttonTitle.utf16Count > 0) {
brOptionItem.setTitle(buttonTitle, forState:UIControlState.Normal)
}
*/
return brOptionItem;
}
In showOptions the animator is assigned an attachment behavior for a brOptionItem that was just created when createButtomItemAtIndex was called.
In createButtonItemAtIndex, the brOptionItem is created and a target was added to execute a function when the button was tapped.
The option buttons work without the animation. I can click them and the target function is executed.
However, when the animation is added and the option buttons are placed where they are, nothing happens when the buttons are tapped.
I am extremely stuck on this. I have no idea why the animation stops the tapping actions. The buttons are supposed to be draggable as well.
Here is my source code. The code I reference can be see in the BROptiosnButton file.
https://github.com/WobbleDev/BROptionsSwift
Here is the reference code I used
https://github.com/BasheerSience/BROptionsButton
Thanks!

Related

How can I create a Pie Chart with rounded slice edges?

I want to achieve the following effect, shown below in my iOS app (written in Swift)
So far I have been able to achieve this, using Charts by danielgindi. But I am not able to get the desired effect as I want to. Is there any way to add rounded corners to each Pie Chart slice like in this example image here?:
My Chart setup is as follows:
let data1 = PieChartDataEntry(value: 3)
let data2 = PieChartDataEntry(value: 5)
let data3 = PieChartDataEntry(value: 4)
let data4 = PieChartDataEntry(value: 6)
let data5 = PieChartDataEntry(value: 8)
let values = [data1, data2, data3, data4, data5]
let chartDataSet = PieChartDataSet(entries: values, label: nil)
let chartData = PieChartData(dataSet: chartDataSet)
let colors = [UIColor.fuelTintColor, UIColor.maintenanceTintColor, UIColor.insuranceTintColor, UIColor.fastagTintColor, UIColor.miscTintColor]
chartDataSet.colors = colors as! [NSUIColor]
chartDataSet.sliceSpace = 10
pieChart1.data = chartData
pieChart1.holeRadiusPercent = 0.8
I think it can be done using the PieChartRenderer but I have no idea how I should proceed.
Also, if you have any suggestions for other ways to implement this do let me know.
Solved it, and here's the output:
ring-chart-img
Basically I am using this library, called CircularProgressView (https://cocoapods.org/pods/CircleProgressView) to achieve the individual rings. Since I needed 5 rings, I stacked 5 such views (with clear background) on top of each other and rotated them to achieve my desired effect.
Cocoapod setup
First, you have to install the CircularProgressView pod in your project. I am using Cocoapods for importing the library here.
NOTE: If you do not have Cocoapods setup already then you need to install Cocoapods first using steps here: https://guides.cocoapods.org/using/getting-started.html
To add it using Cocoapods, add the following line in your Podfile.
pod 'CircleProgressView'
and run pod install on the Terminal from within your project directory.
Interface Builder
In your Storyboard file, go to your View Controller and add a view and set its contents. You can also create this view programmatically but here I will be using the storyboard.
Create 4 more views and use Auto Layout to stack them on top of one another.
Select all 5 views and go to the Identity Inspector (right panel in storyboard, 4th item in top bar).
Set the Class field value (under Custom Class) as 'CircularProgressView'.
Link all 5 views to your ViewController.swift file.
I have named them as follows:
#IBOutlet weak var pieChart1: CircleProgressView!
#IBOutlet weak var pieChart2: CircleProgressView!
#IBOutlet weak var pieChart3: CircleProgressView!
#IBOutlet weak var pieChart4: CircleProgressView!
#IBOutlet weak var pieChart5: CircleProgressView!
Call the function showRingChart() in viewDidLoad() to setup the views.
Here is the code for the functions:
// Function to convert degrees to radian
func degToRad(_ rotationDegrees: Double) -> CGFloat {
let rotationAngle = CGFloat(rotationDegrees * .pi/180.0)
return rotationAngle
}
// Function to Show Ring Chart
func showRingChart() {
// Values for the graph, can be changed as per your need
let val1 = self.val1
let val2 = self.val2
let val3 = self.val3
let val4 = self.val4
let val5 = self.val5
let totalVal = (val1 + val2 + val3 + val4 + val5)
var spacing = 0.05 * totalVal // Spacing is set to 5% (ie. 0.05). Change it according to your needs
print("Spacing: ", spacing)
let totalSpacing = 5 * spacing //Total spacing value to be added in the chart
let total = totalVal + totalSpacing //Total corresponding to 100% on the chart
if val1 == 0.0
&& val2 == 0.0
&& val3 == 0.0
&& val4 == 0.0
&& val5 == 0.0 {
// NO DATA, HIDE ALL CHARTS
pieChart1.isHidden = true
pieChart2.isHidden = true
pieChart3.isHidden = true
pieChart4.isHidden = true
pieChart5.isHidden = true
} else {
// DATA AVAILABLE
// Calculate Percentage of each value in the ring chart (ie. progress)
let valOnePerc = (val1 / total)
let valTwoPerc = (val2 / total)
let valThreePerc = (val3 / total)
let valFourPerc = (val4 / total)
let valFivePerc = (val5 / total)
let spacingPerc = spacing / total
// Angle offsets (in degrees)
let offset1 = (valOnePerc + spacingPerc) * 360
let offset2 = (valOnePerc + valTwoPerc + (2 * spacingPerc)) * 360
let offset3 = (valOnePerc + valTwoPerc + valThreePerc + (3 * spacingPerc)) * 360
let offset4 = (valOnePerc + valTwoPerc + valThreePerc + valFourPerc + (4 * spacingPerc)) * 360
print("--- PRINTING CHART VALUES HERE ---- ")
print(total)
print(valOnePerc)
print(valTwoPerc)
print(valThreePerc)
print(valFourPerc)
print(valFivePerc)
print(offset1)
print(offset2)
print(offset3)
print(offset4)
print("------------------")
// Setup ring chart sections
pieChart1.trackFillColor = UIColor.tintColorOne
pieChart1.progress = valOnePerc
pieChart2.trackFillColor = UIColor.tintColorTwo
pieChart2.progress = valTwoPerc
pieChart2.transform = CGAffineTransform(rotationAngle: degToRad(offset1))
pieChart3.trackFillColor = UIColor.tintColorThree
pieChart3.progress = valThreePerc
pieChart3.transform = CGAffineTransform(rotationAngle: degToRad(offset2))
pieChart4.trackFillColor = UIColor.tintColorFour
pieChart4.progress = valFourPerc
pieChart4.transform = CGAffineTransform(rotationAngle: degToRad(offset3))
pieChart5.trackFillColor = UIColor.tintColorFive
pieChart5.progress = valFivePerc
pieChart5.transform = CGAffineTransform(rotationAngle: degToRad(offset4))
// Unhide all charts
pieChart1.isHidden = false
pieChart2.isHidden = false
pieChart3.isHidden = false
pieChart4.isHidden = false
pieChart5.isHidden = false
}
}
It should do it. Change the (val1, val2, ..., val5) values to change the progress of the rings.

Swift on iOS: Task on global queue adds 2nd task to main queue to display UIImageView, which does not display

I'm trying to display a UIImageView icon that moves across the screen. But icon doesn't display.
viewDidLoad does this:
DispatchQueue.global().async{ app_class.run_app() }
run_app gets next XY location from array sim_locations_track_Array, converts to screen position, and does this to display the UIImageView icon:
DispatchQueue.main.sync{ ... CGRect ... }
But nothing displays.
Here is my code...
import UIKit
class ViewController: UIViewController {
#IBOutlet weak var app_title: UILabel!
static let wrist_band_UIImageView: UIImageView = {
let theImageView = UIImageView()
theImageView.image = UIImage( systemName: "applewatch" )
theImageView.translatesAutoresizingMaskIntoConstraints = false //You need to call this property so the image is added to your view
return theImageView
}()
override func viewDidLoad()
{
print("viewDidLoad...")
super.viewDidLoad()
view.addSubview( ViewController.wrist_band_UIImageView )
DispatchQueue.global().async{
app_class.run_app()
}
}
}
class app_class
{
// X, Y, Z, timestamp in seconds
static let sim_locations_track_Array: [Int] = [ 0, 0, 0, 3,
0, 1, 0, 3,
0, 2, 0, 3,
0, 3, 0, 3,
0, 0, 0, -999 ]
static let items_per_time = 4
static var screen_width = 100
static var screen_height = 100
static var screen_offset_x = 400
static var screen_offset_y = 400
static var screen_location_x = 0
// Run app that animates track of user following sim locations track.
static func run_app()
{
var xcoord = 0
var ycoord = 0
var delay_time_seconds = 1.0
var locations_index = 0
var map_multiplier = 10
var loop = 0
// Loop for each location:
while true
{
// Read XY from locations track:
var board_x = sim_locations_track_Array[ locations_index * items_per_time ]
var board_y = sim_locations_track_Array[ locations_index * items_per_time + 1 ]
var relative_timestamp_seconds = sim_locations_track_Array[ locations_index * items_per_time + 3 ]
var screen_y = screen_height - board_x
var screen_x = screen_width / 2 + board_y
// Not end of data?
if relative_timestamp_seconds > 0
{
DispatchQueue.main.async{
ViewController.wrist_band_UIImageView.frame = CGRect( x: screen_x * map_multiplier,
y: screen_y * map_multiplier,
width: screen_width,
height: screen_height)
}
sleep( UInt32( relative_timestamp_seconds ))
locations_index += 1
}
else // End of locations track data?
{
locations_index = 0
}
}
}
} // end of app_class
You shouldn't call sync on the main thread this leads to deadlock, use async instead.
DispatchQueue.main.sync{
ViewController.wrist_band_UIImageView.frame = CGRect( x: screen_x * map_multiplier,
y: screen_y * map_multiplier,
width: screen_width,
height: screen_height)
}
sync will dispatch work to another queue but the current queue will wait till work is finished. In simple words, sync blocks the current queue. sync(execute:) doc
Your code doesn't actually do anything. You're using a static method on your app_class class, which then fetches a static computed property (wrist_band_UIImageView) of the ViewController class, updates the frame of that newly created computed UIImageView, and then... drops the image view on the floor.
The result of getting a value from ViewController.wrist_band_UIImageView is a newly created UIImageView that is not part of any view hierarchy. Unless you install it into a view hierarchy somewhere, it won't do anything.
Another thing. This code:
DispatchQueue.global().sync() {
sleep( UInt32( relative_timestamp_seconds ))
}
Makes no sense. You're running it from a background thread. Then you invoke another background queue and tell it to synchrnously... go to sleep, for some potentially large number of seconds. What is the point of that? If you want the current background job to sleep, just invoke sleep directly. Dispatching a call to a global sync queue that you just tell to sleep is absurd.
You should take a step back and explain what you're trying to accomplish.

Swift: How do I arrange programatically added checkboxes?

I'm creating an iOS app using Swift, one with checkboxes. Currently, I've placed them inside a view (constrained and all), in the hopes that they would stay there and not mess up the rest of my app. Here's my code so far:
// UI
let lCheckboxHeight: CGFloat = 44.0;
let lCheckboxWidth: CGFloat = 180.0;
let waterSampleTreatmentTitles = ["i - Untreated", "ii - Acidified", "iii - Airfree", "iv - Filtered, Untreated","v - Filtered, Acidified","Stable Isotopes","Others"];
let lNumberOfCheckboxes = waterSampleTreatmentTitles.count
var lFrame = CGRectMake(0, 0, self.view.frame.size.width, lCheckboxHeight);
for (var counter = 0; counter < lNumberOfCheckboxes; counter++) {
let lCheckbox = Checkbox(frame: lFrame, title: waterSampleTreatmentTitles[counter], selected: false);
lCheckbox.mDelegate = self;
lCheckbox.tag = counter;
if (waterSampleTreatmentTitles[counter] == "i - Untreated" && self.flagUntreated == true){
lCheckbox.selected = true
}
else if (waterSampleTreatmentTitles[counter] == "ii - Acidified" && self.flagAcidified == true){
lCheckbox.selected = true
}
else if (waterSampleTreatmentTitles[counter] == "iii - Airfree" && self.flagAirfree == true){
lCheckbox.selected = true
}
else if (waterSampleTreatmentTitles[counter] == "iv - Filtered, Untreated" && self.flagFilterUntreat == true){
lCheckbox.selected = true
}
else if (waterSampleTreatmentTitles[counter] == "v - Filtered, Acidified" && self.flagFilterAcid == true){
lCheckbox.selected = true
}
else if (waterSampleTreatmentTitles[counter] == "Stable Isotopes" && self.flagStabIso == true){
lCheckbox.selected = true
}
else if (waterSampleTreatmentTitles[counter] == "Others" && self.flagOthers == true){
lCheckbox.selected = true
}
self.chemistryProductionWell.viewSampTreat.addSubview(lCheckbox);
lFrame.origin.y += lFrame.size.height;
}
Currently, this creates a list of checkboxes, which spills past the view I made, and just generally makes a mess of the app. The view is long enough for maybe two checkboxes vertically, but not eight.
How do I make it such that the checkboxes are arranged horizontally? I've tried replacing the following code:
lFrame.origin.y += lFrame.size.height;
With this:
lFrame.origin.x += lCheckboxWidth
But that doesn't take into account that the text for the checkboxes aren't the same length, and of course ignores the width restriction as well.
How do I make it such that if the checkbox length exceeds the view, it would drop down to the next line?
Thanks.
If the checkbox has no way to determine its own intrinsic size, you'll have to determine the width that each title would take, then factor that value into that checkbox's frame width.
However, wrapping a row of checkboxes will lead to a couple issues. First, the layout would need to be updated upon autorotation. Second, each row's checkboxes would not be vertically aligned with the previous row's checkboxes.
What you preferably want is a checkbox that can determine its own intrinsic size. Now you'd be able to take advantage of the Auto Layout system, and both the checkboxes, and their containing view could size themselves. This would have avoided the problem where the checkboxes are located outside their container's bounds (as well as having to hard code frames).
At that point, you could benefit from an easier solution like UIStackView, instead of having to code their layout on your own.
If you are using Storyboard, the ideal option would be be a checkbox that could not only self-size, but supported IBDesignable. You could then just setup outlets to IB checkboxes, instead of doing any of this in code.
If you aren't able to find a better control, then the easiest way to do what you ask would be as follows.
let lCheckboxHeight: CGFloat = 44.0
// Make the width as wide as necessary to accommodate the largest title
let lCheckboxWidth: CGFloat = 180.0
// Spacing between checkboxes or rows
let xSpacing: CGFloat = 10
let ySpacing: CGFloat = 10
// Current offset for next checkbox
var xOffset: CGFloat = 0
var yOffset: CGFloat = 0
for counter in 0..<lNumberOfCheckboxes {
var lFrame = CGRect(x: xOffset, y: yOffset, width: lCheckboxWidth, height: lCheckboxHeight);
// Advance offset for upcoming checkbox
xOffset += lCheckboxWidth + xSpacing
// Determine if there is enough room for another horizontal checkbox
let maxX = xOffset + lCheckboxWidth + xSpacing
if maxX >= chemistryProductionWell.viewSampTreat.bounds.maxX {
// Move to start of next row
xOffset = 0
yOffset += lCheckboxHeight + ySpacing
}
// ... Other code here
}
This still doesn't do anything to support autorotation, which is why you should look into an Auto Layout/Adaptive UI approach, and not directly work with frames.
As an aside, although you placed them in a viewSampTreat container, you had based their width on something other than their container, i.e. self.view.frame.size.width. This would have led to a clipping problem.

Scrolling SKReferenceNodes from sks files

I want to scroll SKReferenceNodes created with my sks files I put together in the interface builder. I have scrolled SKSpriteNodes before, and I am trying to use the same code, but my SKReferenceNodes seem to have no frame, and so I can't position them.
Here is my SKReferenceNode setup:
let BG_POINTS_PER_SEC = 50
var dt:NSTimeInterval = 0
let background1 = SKReferenceNode(fileNamed:"BackgroundScene1")
let background2 = SKReferenceNode(fileNamed:"BackgroundScene2")
in didMoveToView
background1.position = CGPointZero;
background2.position = CGPointMake(background1.frame.size.width, 0)
self.addChild(background1)
self.addChild(background2)
in update
moveBackground()
func moveBackground() {
let bgVelocity = CGPoint(x:-BG_POINTS_PER_SEC, y:0)
let amtToMove = bgVelocity *CGFloat(dt)
background1.position = background1.position + amtToMove
background2.position = background2.position + amtToMove
if (background1.position.x <= background1.frame.size.width) {
background1.position.x = CGPointMake(background2.position.x + background2.frame.size.width, background1.position.y)
}
if (background2.position.x <= background2.frame.size.width) {
background2.position.x = CGPointMake(background1.position.x + background1.frame.size.width, background2.position.y)
}
}
Again this works for the scrolling background using a SKSpriteNode, but the SKReferenceNode returns a 0 frame. Is this even possible to scroll SKReferenceNodes from sks files like this?

Update the rotation of a CALayer

I am trying to update the current rotation (and sometimes the position) of a CALayer.
What I am trying to in a couple of simple steps:
Store a couple of CALayers in an array, so I can reuse them
Set the anchor point of all CALayers to 0,0.
Draw CALayer objects where the object starts at a position on a circle
The layers are rotated by the same angle as the circle at that position
Update the position and rotation of the CALayer to match new values
Here is a piece of code I have:
lineWidth is the width of a line
self.items is an array containing the CALayer objects
func updateLines() {
var space = 2 * M_PI * Double(circleRadius);
var spaceAvailable = space / (lineWidth)
var visibleItems = [Int]();
var startIndex = items.count - Int(spaceAvailable);
if (startIndex < 0) {
startIndex = 0;
}
for (var i = startIndex; i < self.items.count; i++) {
visibleItems.append(self.items[i]);
}
var circleCenter = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame));
/* Each line should move up and rotate accordin to this value */
var anglePerLine: CGFloat = (360 / CGFloat(visibleItems.count)).toRadians()
/* Starting position, 270 degrees is on top */
var startAngle: CGFloat = CGFloat(270).toRadians();
/* Lines default rotation, we rotate it to get the right side up */
var lineAngle: CGFloat = CGFloat(180).toRadians();
for (var itemIndex = 0; itemIndex < visibleItems.count; itemIndex++) {
var itemLayer = self.itemLayers[itemIndex];
itemLayer.opacity = 1 - ((0.9 / visibleItems.count) * itemIndex);
/* Calculate start position of layer */
var x = CGFloat(circleRadius) * cos(startAngle) + CGFloat(circleCenter.x);
var y = CGFloat(circleRadius) * sin(startAngle) + CGFloat(circleCenter.y);
var height = CGFloat((arc4random() % 80) + 10);
/* Set position and frame of layer */
itemLayer.frame = CGRectMake(CGFloat(x), CGFloat(y), CGFloat(lineWidth), height);
itemLayer.position = CGPointMake(CGFloat(x), CGFloat(y));
var currentRotation = CGFloat((itemLayer.valueForKeyPath("transform.rotation.z") as NSNumber).floatValue);
var newRotation = lineAngle - currentRotation;
var rotationTransform = CATransform3DRotate(itemLayer.transform, CGFloat(newRotation), 0, 0, 1);
itemLayer.transform = rotationTransform;
lineAngle += anglePerLine;
startAngle += anglePerLine;
}
}
The result of the first run is exactly as I want it to be:
The second run through this code just doesn't update the CALayers correctly and it starts to look like this:
I think it has to do with my code to update the location and transform properties of the CALayer, but whatever I do, it always results in the last picture.
Answered via Twitter: setting frames and transform is mutually exclusive. Happy to help. Finding my login credentials for SO is harder. :D
Found the answer thanks to #iosengineer on Twitter. When setting a position on the CALayer, you do not want to update the frame of the layer, but you want to update the bounds.
Smooth animation FTW

Resources