I am trying to swipe up to a particular element in my app, if i use "swipeup" it is going to bottom of the view, which i don't want
Here is my code :
XCUIElement *staticText = [[[tablesQuery2 childrenMatchingType:XCUIElementTypeCell] elementBoundByIndex:2] childrenMatchingType:XCUIElementTypeStaticText].element;
[staticText swipeUp];
Here is my app screen before using swipe up
Here is my app screen after using swipe up
If you know which element you want to select and the height of each picker item, you can make an extension of XCUIElement to select the right value.
/// Move up/down the picker options until the given `selectionPosition` is reached.
func changePickerSelection(pickerWheel: XCUIElement, selectionPosition: UInt) {
// Select the new value
var valueSelected = false
while !valueSelected {
// Get the picker wheel's current position
if let pickerValue = pickerWheel.value {
let currentPosition = UInt(getPickerState(String(pickerValue)).currentPosition)
switch currentPosition.compared(to: selectionPosition) {
case .GreaterThan:
pickerWheel.selectPreviousOption()
case .LessThan:
pickerWheel.selectNextOption()
case .Equal:
valueSelected = true
}
}
}
}
/// Extend XCUIElement to contain methods for moving to the next/previous value of a picker.
extension XCUIElement {
/// Scrolls a picker wheel up by one option.
func selectNextOption() {
let startCoord = self.coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: 0.5))
let endCoord = startCoord.coordinateWithOffset(CGVector(dx: 0.0, dy: 30.0)) // 30pts = height of picker item
endCoord.tap()
}
/// Scrolls a picker wheel down by one option.
func selectPreviousOption() {
let startCoord = self.coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: 0.5))
let endCoord = startCoord.coordinateWithOffset(CGVector(dx: 0.0, dy: -30.0))
endCoord.tap()
}
}
let pickerWheel = app.pickerWheels.element(boundBy: 0)
changePickerSelection(pickerWheel, selectionPosition: 2)
Related
I'm using a UIScrollView to display an image with various markers on top. The image view has a UILongPressGestureRecognizer that detects long presses. When the long press event is detected, I want to create a new marker at that location.
The problem I'm having is that when I zoom in or out, the location of the gesture recognizer's location(in: view) seems to be off. Here's a snippet of my implementation:
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.onLongPress(gesture:)))
self.hostingController.view.addGestureRecognizer(longPressGestureRecognizer)
#objc func onLongPress(gesture: UILongPressGestureRecognizer) {
switch gesture.state {
case .began:
guard let view = gesture.view else { break }
let location = gesture.location(in: view)
let pinPointWidth = 32.0
let pinPointHeight = 42.0
let x = location.x - (pinPointWidth / 2)
let y = location.y - pinPointHeight
let finalLocation = CGPoint(x: x, y: y)
self.onLongPress(finalLocation)
default:
break
}
}
Please note that I'm using a UIViewControllerRepresentable that contains a UIViewController with a UIScrollView that is surfaced to my SwiftUI View. Maybe this might be causing it.
Here's the SwiftUI code:
var body: some View {
UIScrollViewWrapper(scaleFactor: $scaleFactor, onLongPress: onInspectionCreated) {
ZStack(alignment: .topLeading) {
Image(uiImage: image)
ForEach(filteredInspections, id: \.syncToken) { inspection in
InspectionMarkerView(
scaleFactor: scaleFactor,
xLocation: CGFloat(inspection.xLocation),
yLocation: CGFloat(inspection.yLocation),
iconName: iconNameForInspection(inspectionMO: inspection),
label: inspection.readableIdPaddedOrNewInspection)
.onTapGesture {
selectedInspection = inspection
}
}
}
}
.clipped()
}
Here's a link to a reproducible example project:
https://github.com/Kukiwon/sample-project-zoom-long-press-location
Here's a recording of the problem:
Link to video
Any help is greatly appreciated!
I don't use SwiftUI, but I've seen some quirky stuff looking at UIHostingController implementations, and it appears a specific quirk is hitting you.
I inset your scroll view by 40-pts and gave the main view a red background to make it a little easier to see what's going on.
First, add a 2-pixel blue line around the border of your sheet_hd image, and scroll all the way to the bottom-left corner. It should look like this:
As you zoom in, keeping the scroll at bottom-left, it will look like this:
So far, so good -- and using a long-press to add a marker works as expected.
However, as soon as we zoom out to less than 1.0 zoom scale:
we can no longer see the bottom edge of the image.
Zooming back in makes it more obvious:
And the long-press location is incorrect.
For further clarification, if we set .clipsToBounds = false on the scroll view, and set .alpha = 0.5 on the image view, we see this:
We can drag the view up to see the bottom edge, but as soon as we release the touch is bounces back below the frame of the scroll view.
What should fix this is to use this extension:
// extension to remove safe area from UIHostingController
// source: https://stackoverflow.com/a/70339424/6257435
extension UIHostingController {
convenience public init(rootView: Content, ignoreSafeArea: Bool) {
self.init(rootView: rootView)
if ignoreSafeArea {
disableSafeArea()
}
}
func disableSafeArea() {
guard let viewClass = object_getClass(view) else { return }
let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
if let viewSubclass = NSClassFromString(viewSubclassName) {
object_setClass(view, viewSubclass)
}
else {
guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
let safeAreaInsets: #convention(block) (AnyObject) -> UIEdgeInsets = { _ in
return .zero
}
class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
}
objc_registerClassPair(viewSubclass)
object_setClass(view, viewSubclass)
}
}
}
Then, in viewDidLoad() in your UIScrollViewViewController:
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(self.scrollView)
self.pinEdges(of: self.scrollView, to: self.view)
// add this line
self.hostingController.disableSafeArea()
self.hostingController.willMove(toParent: self)
self.scrollView.addSubview(self.hostingController.view)
self.pinEdges(of: self.hostingController.view, to: self.scrollView)
self.hostingController.didMove(toParent: self)
self.hostingController.view.alpha = 0
}
Quick testing (obviously, you'll want to thoroughly test it) seems good... we can scroll all the way to the bottom, and long-press location is back where it should be.
I am working on UIPanGestureRecognizer and to me it is working. but I have some problem here as I am new to iOS and just shifted from Android to iOS.
First take a look at what I want to do:
What I want: I have a UITableView and I want to perform swiping on the Cells. I just want to drag them from left to right side and move/Delete that cell. Pretty same like it is done in android.
But I just want to move the item only in one direction. And that is "LEFT TO RIGHT". But not from right to left. Now here take a look at what I have done so far
What I have Done:
#objc func handlePan(recognizer: UIPanGestureRecognizer) {
// 1
if recognizer.state == .began {
// when the gesture begins, record the current center location
originalCenter = center
print("Center",originalCenter)
}
// 2
if recognizer.state == .changed {
let translation = recognizer.translation(in: self)
center = CGPoint(x: originalCenter.x+translation.x, y: originalCenter.y)
// has the user dragged the item far enough to initiate a delete/complete?
deleteOnDragRelease = frame.origin.x < -frame.size.width / 2.0
completeOnDragRelease = frame.origin.x > frame.size.width / 2.0
// print ("FrameX = ",frame.origin.x , " , ","Width = ",frame.size.width / 2.0 , "Total = ",frame.origin.x < -frame.size.width / 2.0 )
//print ("DelOnDrag = ",deleteOnDragRelease , " , ","CompOnDrag = ",completeOnDragRelease)
}
// 3
if recognizer.state == .ended {
// the frame this cell had before user dragged it
let originalFrame = CGRect(x: 0, y: frame.origin.y,
width: bounds.size.width, height: bounds.size.height)
if deleteOnDragRelease {
if delegate != nil && clickedItem != nil {
// notify the delegate that this item should be deleted
delegate!.toDoItemDeleted(clickedItem: clickedItem!)
}
} else if completeOnDragRelease {
UIView.animate(withDuration: 8.2, animations: {self.frame = originalFrame})
} else {
UIView.animate(withDuration: 8.2, animations: {self.frame = originalFrame})
}
}
}
I know I can make a check on ".changed" , and calculate if the X value is going towards 0 or lesser then 0. But point is for some time it will move item from right to left.
Question: Is there any way I can get the x value of point of contact? or just some how I can get user want to swipe right to left and just stop user from doing that?? Please share your knowledge
your same code just one changes in your UIGestureRecognizer method replace with this code and your problem solve. only left to right side swap work on your tableview cell . any query regrading this just drop comment below.
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer {
let translation = panGestureRecognizer.translation(in: superview!)
if translation.x >= 0 {
return true
}
return false
}
return false
}
Good Luck.
Keep coding.
You can do this using below extensions
extension UIPanGestureRecognizer {
enum GestureDirection {
case Up
case Down
case Left
case Right
}
func verticalDirection(target: UIView) -> GestureDirection {
return self.velocity(in: target).y > 0 ? .Down : .Up
}
func horizontalDirection(target: UIView) -> GestureDirection {
return self.velocity(in: target).x > 0 ? .Right : .Left
}
}
And you can get direction like below
gestureRecognizer.horizontalDirection(target: self)
Currently have a popup view which sets 3 picker values by tapping on the SET button:
However, I want to remove the SET button altogether, and have the picker values set upon tapping outside of the popup, which in turn hides the popup.
Here is the current code:
// function for selecting picker values
func pickerDidSet() {
let focusPeriodChoice = focusPeriodDataSource[pickerView.selectedRow(inComponent: 0)]
let breakPeriodChoice = breakPeriodDataSource[pickerView.selectedRow(inComponent: 1)]
let repeatCountChoice = repeatCountDataSource[pickerView.selectedRow(inComponent: 2)]
persistPickerChoice(focusPeriodChoice, dataType: .focusPeriod)
persistPickerChoice(breakPeriodChoice, dataType: .breakPeriod)
persistPickerChoice(repeatCountChoice, dataType: .repeatCount)
timerSummaryLabel.text = "\(focusPeriodChoice)m • \(breakPeriodChoice)m • \(repeatCountChoice)x"
UIView.animate(withDuration: 0.2, animations: { self.pickerContainerView.alpha = 0.0 }, completion: { finished in
self.pickerContainerView.isHidden = true
})
}
// Open popup, by tapping gear icon
#IBAction func openSettings(_ sender: Any) {
pickerView.selectRow(pickerChoiceIndex(forDataType: .focusPeriod), inComponent: 0, animated: false)
pickerView.selectRow(pickerChoiceIndex(forDataType: .breakPeriod), inComponent: 1, animated: false)
pickerView.selectRow(pickerChoiceIndex(forDataType: .repeatCount), inComponent: 2, animated: false)
self.pickerContainerView.isHidden = false
UIView.animate(withDuration: 0.2) {
self.pickerContainerView.alpha = 1.0
}
}
// Once pickers have been set, display the summary
private func configureSummaryLabel() {
let focusPeriodChoice = pickerChoice(forDataType: .focusPeriod)
let breakPeriodChoice = pickerChoice(forDataType: .breakPeriod)
let repeatCountChoice = pickerChoice(forDataType: .repeatCount)
timerSummaryLabel.text = "\(focusPeriodChoice)m • \(breakPeriodChoice)m • \(repeatCountChoice)x"
}
// Setting the picker “SET” button
private func addPickerSetButton(atX x: CGFloat, centerY: CGFloat) {
pickerSetButton.frame = CGRect(x: x, y: 0, width: 40, height: 20)
pickerSetButton.center = CGPoint(x: pickerSetButton.center.x, y: centerY)
pickerSetButton.setTitle("SET", for: .normal)
pickerSetButton.setTitleColor(UIColor.white, for: .normal)
pickerSetButton.setTitleColor(UIColor.darkGray, for: .highlighted)
pickerSetButton.titleLabel?.font = UIFont.boldSystemFont(ofSize: 17)
pickerSetButton.addTarget(self, action: #selector(pickerDidSet), for: .touchUpInside)
pickerHeaderView.addSubview(pickerSetButton)
}
If the Previous Black View is you default view of ViewController then all you need is to implemented below method.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// Check that the touched view is your background view
if touches.first?.view == self.view {
// Do What Every You want to do
}
}
Detail
Every ViewController has a default view object. As in your case the black overlay displaying behind your popup seems like the default view of that view controller. If that black overlay is not your default view then create and IBOutlet of that view which is black opacified in color. And then in the above method where you are check that which view is touch check that if touched view is your black view or not.
Suppose you black view's IBOutlet is backgroundView then the above check will be something like this.
if touches.first?.view == self.backgroundView {
//It means you have touched outside the pop and out side the pop there is only your backgroundView.
//Here you should do exactly the same which you were doing when `SET` button was clicked.
}
touchesBegan method didn't work if touched object is a button so as per you logic.
You need to check if the PickerView is visible then disable it instead of firing the other feature of that button.
Example.
Create a boolean variable named isPickerViewVisible in your class and when picker view is going to visible make it true and when picker view is getting hide just make it false. There might be an IBAction for that red button.
#IBAction didTapButton(_ sender: Any){
//Here you need to check if pickerView is open then disable it. I don't know what logic you have implemented to show picker view.
if isPickerViewVisible {
self.pickerDidSet()
}else {
//Here you should do the task that you do on clicking this button.
}
}
I am a beginner in swift. I am building a single view tip calculator app and i only want some animation to happen when the app loads for the first time. And not happen every time i edit something.
Here is my code:
I am hiding the container outside the view when the app loads
override func viewWillAppear(animated: Bool) {
super.viewWillAppear(animated)
billView.center.y -= (billView.frame.height)/2
}
And animating it to move down
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
UIView.animateWithDuration(0.8, delay: 0.0,
options: [.CurveEaseOut], animations: {
self.billView.center.y = 210 }, completion: nil)
print(billView.center.y)
}
This works fine when the app loads. Unfortunately this animation keeps happening again and again anytime I interact with the app.
Like this animation happens when this function is called (which is when any information is entered on the text field or the slider is moved)
let billAmount = NSString(string: billField.text!).doubleValue
var tipPercentages = [0.15, 0.18, 0.22]
let tipPercentage = tipPercentages[segTipControl.selectedSegmentIndex]
let percentLabel = round(tipPercentage * 100)
selectedbillPercentageLabel.text = "+\(percentLabel)%"
let tipAmount = billAmount * tipPercentage
let totalAmount = billAmount + tipAmount
let roundedTotalAmount = round(totalAmount * 100)/100
totalLabel.text = "$\(roundedTotalAmount)"
let splitSelected = Double(splitValueSlider.value)
let intsplitSelected = Int(splitSelected)
splitLabel.text = "\(intsplitSelected)"
let splitDollarAmount = roundedTotalAmount/splitSelected
let roundedSplitDollarAmount = round(splitDollarAmount * 100)/100
splitAmount.text = "$\(roundedSplitDollarAmount)"
Is there a way to avoid calling the method viewDidLayoutSubviews() when any data is entered in the app.
animated gif
Rather than avoiding calling viewDidLayourSubviews, you could add a simple boolean flip to show your animation only once. Add a boolean property that you initialize to false; show animation on condition of this property being false, and flip the boolean after the animation has been shown.
var animationHasBeenShown = false // class property
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if !animationHasBeenShown {
UIView.animateWithDuration(0.8, delay: 0.0,
options: [.CurveEaseOut], animations: {
self.billView.center.y = 210 }, completion: nil)
print(billView.center.y)
animationHasBeenShown = true
}
}
Alternatively, consider placing your animation in another context; does it really need to be in viewDidLayoutSubviews() (depends on the context of your app, but possibly rather in viewDidAppear()?)
I am trying to replicate a pull to refresh on a UITableView using the new Xcode UI testing framework in Xcode 7 (beta 3)
My current approach is dragging from the table to whatever element below the table I can find. This works when there is a fixed item below the table like a UIToolbar or UITabBar I would rather not rely on having a UITabBar or UIToolbar but I can't figure out a way to do the pull to refresh/drag action without using the method in XCUIElement.
func pressForDuration(duration: NSTimeInterval, thenDragToElement otherElement: XCUIElement)
But it fails when I don't have a toolbar/tabbar and try to drag using the cells
This is the relevant portion of my code:
func testRefresh() {
//For testing cell
for _ in 0...9 {
addCell()
}
refreshTable()
}
func refreshTable(var tbl: XCUIElement? = nil) {
let app = XCUIApplication()
if tbl == nil {
let tables = app.tables
if tables.count > 0 {
tbl = tables.elementAtIndex(0)
}
}
guard let table = tbl else {
XCTFail("Cannot find a table to refresh, you can provide on explicitly if you do have a table")
return
}
var topElement = table
let bottomElement: XCUIElement?
//Try to drag to a tab bar then to a toolbar then to the last cell
if app.tabBars.count > 0 {
bottomElement = app.tabBars.elementAtIndex(0)
}
else if app.toolbars.count > 0 {
bottomElement = app.toolbars.elementAtIndex(0)
}
else {
let cells = app.cells
if cells.count > 0 {
topElement = cells.elementAtIndex(0)
bottomElement = cells.elementAtIndex(cells.count - 1)
}
else {
bottomElement = nil
}
}
if let dragTo = bottomElement {
topElement.pressForDuration(0.1, thenDragToElement: dragTo)
}
}
func addCell() {
let app = XCUIApplication()
app.navigationBars["Master"].buttons["Add"].tap()
}
Additional failed attempts:
swipeDown() (multiples times as well)
scrollByDeltaX/deltaY (OS X only)
You can use the XCUICoordinate API to interact with specific points on the screen, not necessarily tied to any particular element.
Grab a reference to the first cell in your table. Then create a coordinate with zero offset, CGVectorMake(0, 0). This will normalize a point right on top of the first cell.
Create an imaginary coordinate farther down the screen. (I've found that a dy of six is the smallest amount needed to trigger the pull-to-refresh gesture.)
Execute the gesture by grabbing the first coordinate and dragging it to the second one.
let firstCell = app.staticTexts["Adrienne"]
let start = firstCell.coordinateWithNormalizedOffset(CGVectorMake(0, 0))
let finish = firstCell.coordinateWithNormalizedOffset(CGVectorMake(0, 6))
start.pressForDuration(0, thenDragToCoordinate: finish)
More information along with a working sample app is also available.
I managed to do such tasks with coordinates like Joe suggested. But i went a step further using coordinateWithOffset()
let startPosition = CGPointMake(200, 300)
let endPosition = CGPointMake(200, 0)
let start = elementToSwipeOn.coordinateWithNormalizedOffset(CGVectorMake(0, 0)).coordinateWithOffset(CGVector(dx: startPosition.x, dy: startPosition.y))
let finish = elementToSwipeOn.coordinateWithNormalizedOffset(CGVector(dx: 0, dy: 0)).coordinateWithOffset(CGVector(dx: endPosition.x, dy: endPosition.y))
start.pressForDuration(0, thenDragToCoordinate: finish)
Now i am able to drag from a specific point to another specific point. I implemented a custom refresh on some of my views. While implementing this, i also discovered that i can even use this to access the control center or the top menu.
Here is a Swift 5.1 version
let firstCell = staticTexts["Id"]
let start = firstCell.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0))
let finish = firstCell.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 100))
start.press(forDuration: 0, thenDragTo: finish)