I'm trying to implement adding new portion of information at the top, when user is scrolling to the top (to see chat history) without any jumping like in Telegram, Whatsup and so on
Here is my method
private var toItem: NSIndexPath?
private func addMoreCells () {
if collectionView.contentOffset.y <= 0 {
guard let userChatUid = user?.chatUid else { return }
guard let ownChatUid = UsersManager.sharedInstance.currentChatUID() else { return }
guard let offset = conversation?.messages.count else { return }
ConversationsManager.sharedInstance.getConversationsWithMoreMessagesFor(ownChatUid, recipient: userChatUid, offset: offset - 1, successHandler: { [weak weakSelf = self] (conversations) in
dispatch_async(dispatch_get_main_queue()) {
if let messagesFromServer = conversations.first?.messages, var messages = weakSelf?.conversation?.messages {
for message in messagesFromServer {
if !messages.contains(message) {
messages.append(message)
}
}
messages = messages.sort({$0.time?.compare($1.time!) == .OrderedAscending})
weakSelf?.conversation?.messages = messages
weakSelf?.collectionView.reloadData()
weakSelf?.toItem = NSIndexPath(forItem: (messages.count - messagesFromServer.count), inSection: 0)
if let item = weakSelf?.toItem{
weakSelf?.collectionView.scrollToItemAtIndexPath(item, atScrollPosition: .Top, animated: false)
}
}
}
}, failHandler: { (error) in
print(error)
})
}
}
which is launched from
func scrollViewDidEndDecelerating(scrollView: UIScrollView) {
addMoreCells()
}
First time when I'm scrolling to the top It works as I expect (new portion is added, no jumping. I can see new data and can continue scrolling to the top), but when I continue scrolling to the top and new data are added, it always returns to the position, when first info was added.
But I expect It should return to value messages.count - messagesFromServer.count. In xCode I see numbers are changed every time when I'am scrolling, but scrollToItemAtIndexPath doesn't work properly
And Every time when I scroll to the top and new data are added, It always returns to first position when first info was added.
Can Anyone know what's problem? Cause I cant find solutions to fix it.
I've solved this problem like this:
After reloadData() I save contentSize of collection view, after that I refresh layout, get new contentSize and subtract them to get needed offset to scroll to this place without jumping.
weakSelf?.collectionView.reloadData()
guard let previousContentSize = weakSelf?.collectionView.contentSize else { return }
weakSelf?.collectionView.collectionViewLayout.invalidateLayout()
weakSelf?.collectionView.collectionViewLayout.prepareLayout()
weakSelf?.collectionView.layoutIfNeeded()
guard let newContentSize = weakSelf?.collectionView.contentSize else { return }
print("previous Content Size \(previousContentSize) and new Content Size \(newContentSize)")
let contentOffset = newContentSize.height - previousContentSize.height
weakSelf?.collectionView.setContentOffset(CGPoint(x: 0.0, y: contentOffset), animated: false)
I works great and as I would expect.
Related
Good day, guys!
I have a little problem with updating data.
I'm getting information from backend with JSON-RPC and populating my table view cells.
I implemented UIRefreshContol to my TableView, so now when I pull to refresh it gives me the new information on top of old one.
So I have a list of products and when I add some products and refresh tableView to get updated information, tableView gives me old info AND new one on top of the old one
Are there any ways to empty table before I will get updated information from JSON?
// That's my JSON
func getJSONresponse() {
getResponse(o_method: "search_read", o_model: "res.users", o_domain:[["id", "=", jkID]], o_fields: ["name","partner_id"]) { (result) -> () in
//print(result)
if result != JSON.null {
let partner_id = result[0]["partner_id"][0].int!
getResponse(o_method: "search_read", o_model: "res.partner", o_domain: [["id", "=", partner_id]], o_fields: ["name", "kato"], completion: { (result)->() in
//print(result)
let id = result[0]["kato"][0].int!
katoID = id
print("adiasodjasoid",katoID)
getResponse(o_method: "search_read", o_model: "property.building", o_domain: [], o_fields: ["name","kato_area"], completion: { (result)->() in
result.forEach({ (temp) in
var area:String = "", name:String = "", totID: Int = 0
if (temp.1["kato_area"].string != nil) {
area = temp.1["kato_area"].string!
}
if (temp.1["name"].string != nil) {
name = temp.1["name"].string!
}
if (temp.1["id"].int != nil) {
totID = temp.1["id"].int!
}
let newModel = resUser(area: area, name: name, id: totID)
self.data.append(newModel)
})
DispatchQueue.main.async {
self.tableView.reloadData()
}
// print(self.data)
})
})
}
}
}
That's my Pull to refresh Function
#objc func refreshData() {
tableView.reloadData()
getJSONresponse()
self.tableView.contentInset = UIEdgeInsets(top: 80, left: 0, bottom: 0, right: 0)
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) {
self.refresher.endRefreshing()
self.tableView.contentInset = UIEdgeInsets.zero
self.tableView.setContentOffset(.zero, animated: true)
}
}
Well you will have to bind the UITableView with to an array, some may also refer to it to as datasource.
So basically heres the flow:
1) Create a mutable array/datasource (This will hold all the JSON).
2) Correspond UITableView number of rows method to return the count this array/datasource.
3) When you press the refresh. remove all the contents of the array/datasource (removeAll)
4) Once you receive the response from backend, add them to the array/datasource and call reloadData on UITableView object.
Hope That Helps. Code and Prosper!!
Please bind UITableView with an Array. Then update this Array first from the data that you are getting from JSON (Empty it and put in the new data), and then call UITableView reload.
I am trying to add uitest for refresh control but I could not have done it because of I can't get access to refresh control with accessibility identifier (refreshControl.accessibilityIdentifier = "refreshControlList")
launchAppWithArgs(json: OrdersResultJSON.ok, xcuiApp: app)
let table = app.tables["agendaTable"]
DispatchQueue.main.async {
table.otherElements["sectionHeader0"].press(forDuration: 2, thenDragTo: table.otherElements["sectionHeader2"])
}
if !table.otherElements[""].waitForExistence(timeout: 6) {
print("XXX")
}
Any suggestion to test it?
To perform a pull to refresh, simply get the first cell and drag it down:
let firstCell = app.tables["agendaTable"].cells.firstMatch
let start = firstCell.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0))
let finish = firstCell.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 10))
start.press(forDuration: 0, thenDragTo: finish)
Hope this helps!
You could grab the UIRefreshControl's title label and put an identifier on it, like this:
func setSubviewAccessibility(for tableView: UITableView) {
guard let titleLabel = tableView.refreshControl?.subviews.first?.subviews.last as? UILabel else {
return
}
titleLabel.isAccessibilityElement = true
titleLabel.accessibilityIdentifier = "refresh_control_label"
}
Call this method passing the table view that you've set the UIRefreshControl for and you should be good to go.
Then, on the test side:
let refreshControl = XCUIApplication().staticTexts["refresh_control_label"]
guard refreshControl.waitForExistence(timeout: 5) else {
return XCTFail("Refresh control label not found.")
}
The only problem remaining is you'll probably need the list loading to take a little longer than a second so that your tests don't miss the UIRefreshControl. It's always good to use waitForExistence.
I'm trying to animate something that spins / left to right, but whenever I call
spinLeft() or spinRight() then the animation always starts from frame 0.
In other words, I want to be able to say spin something 4 out of 10 frames, stop, then
spin in the opposite direction, FROM frame 4. Right now, it resets to frame 0.
var textures = [SKTexture]() // Loaded with 10 images later on.
var sprite = SKSpriteNode()
func spinLeft() {
let action = SKAction.repeatForever(.animate(with: textures, timePerFrame: 0.1))
sprite.run(action)
}
func spinRight() {
let action = SKAction.repeatForever(.animate(with: textures, timePerFrame: 0.1)).reversed()
sprite.run(action)
}
You could do this (Syntax may be a little off, but you get the point):
The key here is the .index(of: ... ) which will get you the index.
func spinUp() {
let index = textures.index(of: sprite.texture)
if index == textures.count - 1 {
sprite.texture = textures[0]
}
else {
sprite.texture = textures[index + 1]
}
}
func spinDown() {
let index = textures.index(of: sprite.texture)
if index == 0 {
sprite.texture = textures[textures.count - 1]
}
else {
sprite.texture = textures[index - 1]
}
}
func changeImage(_ isUp: Bool, _ amountOfTime: CGFloat) {
let wait = SKAction.wait(duration: amountOfTime)
if isUp {
run(wait) {
self.imageUp()
}
}
else {
run(wait) {
self.imageDown()
}
}
}
If you use something like a swipe gesture recognizer, you can use it's direction to set the isUp Bool value and the velocity of that swipe for the amountOfTime for the changeImage function.
The changeImage will only change the image once, so you will need to handle this somewhere else, or create another function if you want it to continuously spin or die off eventually.
Hope this helps!
I need to scroll the table to bottom.When i write the code of scrolling the table in viewwilAppear it does not work,but when i write the same code in viewDidAppear it works but user see the scrolling. I do not want to see the user , scrolling how to make tableScrolling in ViewWillAppear.`
override func viewWillAppear(animated: Bool)
{
super.viewWillAppear(animated)
UIApplication.sharedApplication().delegate!.window!!.windowLevel = UIWindowLevelNormal
self.view.backgroundColor = UIColor.whiteColor()
viewWillAppearExecuted = true
executeCodeOfViewViewAppear()
}
func executeCodeOfViewViewAppear() {
if viewWillAppearExecuted {
viewWillAppearExecuted = false
if appNeedsAutoResize{
self.noLongerParticipantMessageLabel.font = UIUtils.getFontForApproprieteField(.Footnote).font
self.typingStatusLabel.font = UIUtils.getFontForApproprieteField(.Headline).font
}
withAnimationFlag = true
if !self.composeBar.textView.isFirstResponder()
{
// This block will execute if compose bar is first responder
self.updateTableBottomCnt(value:self.composeBar.frame.size.height)
// Update compose bar constarint
if self.composeBarBottomConstraing.constant != 0
{
self.composeBarBottomConstraing.constant = 0
}
}
self.isOpenNextScreen = false
self.checkIfloginUserisActiveMember()
self.tabBarController?.tabBar.hidden = true
ChatCommonCall.sharedInstance.currentThreadIdForPush = self.threadInfo.threadId
// This block is used for make a responder to keyboard for send message.
if shouldComposeBarFirstResponder == true {
self.composeBar.becomeFirstResponder()
shouldComposeBarFirstResponder = false
}
self.showMessageById(self.messageIdForMessageDisplay)
ChatCommonCall.sharedInstance.isChatModuleScreen = true
self.changeFrame()
self.composeBar.button.enabled = self.composeBar.text.trim().length > 0 ? true : false
/// Table frame changes.
switch self.optType
{
case .NewMessageSection:
break
case .DoNotHandle :
break
case .None :
if !(self.messageIdForMessageDisplay.isEqual(Guid.emptyGuid()))
{
self.optType = .ShowMessageOfID(id: self.messageIdForMessageDisplay)
}
else {
self.optType = .ScrollToBottom
}
break
default :
break
}
switch self.optType
{
case .NewMessageSection :
break
default :
self.handleOptType()
}
//Implemented the new observer pattern in the whole project where we suppose to refresh the screen for background sync data
//By Nikhil Kumar Saraf on 19 July 2016
Notifier.Unregister()
let observer = DatabaseObserverInfo(tName: ["chatthread","chatthreadmember","chatmessage","chatmessagereceiver","chatthumbnail","pfthumbnail","chatmessagemarker"], oType: [SyncOpType.Insert,SyncOpType.Update,SyncOpType.Delete], observer: self, stick: false, syncTrigger: SyncTriggerType.Post)
Notifier.Register(observer)
//By Balkrishan Yadav for AutoResizing UI on 30 Nov. 2016
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ChatMessageController.didChangePreferredContentSize(_:)), name:UIContentSizeCategoryDidChangeNotification, object: nil)
// This condition will execute if some one sharing image and coming to the chat message screen
//#Balkrishan Yadav
if ChatCommonCall.sharedInstance.attachmentInfo != nil {
self.addFileInComposeBar(ChatCommonCall.sharedInstance.attachmentInfo!)
ChatCommonCall.sharedInstance.attachmentInfo = nil
let dispatchTime: dispatch_time_t = dispatch_time(DISPATCH_TIME_NOW, Int64(0.3 * Double(NSEC_PER_SEC)))
dispatch_after(dispatchTime, dispatch_get_main_queue(), {
// your function here
// self.changeController(true)
})
}
}
}
func handleOptType(callFromViewDidAppear:Bool = false) {
switch optType {
case .ScrollToBottom : // No new message is available. show last message. self.messageTableView.setContentOffset(CGPointMake(0,CGFloat.max), animated: !isCameFromThreadComposer)
break
default:
// do nothing
}
If you don't want the scrolling to be animated you just have to pass false as the animated argument to the scrolling function. For example :
tableView.scrollToRow(at: indexPath, at: .top, animated: false)
This function would scroll the indexPath's row to the top of the table view without any animation.
I am automating an app using UI Testing in Xcode 7. I have a scrollview with XCUIElements (including buttons, etc) all the way down it. Sometimes the XCUIElements are visible, sometimes they hidden too far up or down the scrollview (depending on where I am on the scrollview).
Is there a way to scroll items into view or maybe tell if they are visible or not?
Thanks
Unfortunately Apple hasn't provided any scrollTo method or a .visible parameter on XCUIElement. That said, you can add a couple helper methods to achieve some of this functionality. Here is how I've done it in Swift.
First for checking if an element is visible:
func elementIsWithinWindow(element: XCUIElement) -> Bool {
guard element.exists && !CGRectIsEmpty(element.frame) && element.hittable else { return false }
return CGRectContainsRect(XCUIApplication().windows.elementBoundByIndex(0).frame, element.frame)
}
Unfortunately .exists returns true if an element has been loaded but is not on screen. Additionally we have to check that the target element has a frame larger than 0 by 0 (also sometimes true) - then we can check if this frame is within the main window.
Then we need a method for scrolling a controllable amount up or down:
func scrollDown(times: Int = 1) {
let topScreenPoint = app.mainWindow().coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: 0.05))
let bottomScreenPoint = app.mainWindow().coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: 0.90))
for _ in 0..<times {
bottomScreenPoint.pressForDuration(0, thenDragToCoordinate: topScreenPoint)
}
}
func scrollUp(times: Int = 1) {
let topScreenPoint = app.mainWindow().coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: 0.05))
let bottomScreenPoint = app.mainWindow().coordinateWithNormalizedOffset(CGVector(dx: 0.5, dy: 0.90))
for _ in 0..<times {
topScreenPoint.pressForDuration(0, thenDragToCoordinate: bottomScreenPoint)
}
}
Changing the CGVector values for topScreenPoint and bottomScreenPoint will change the scale of the scroll action - be aware if you get too close to the edges of the screen you will pull out one of the OS menus.
With these two methods in place you can write a loop that scrolls to a given threshold one way until an element becomes visible, then if it doesn't find its target it scrolls the other way:
func scrollUntilElementAppears(element: XCUIElement, threshold: Int = 10) {
var iteration = 0
while !elementIsWithinWindow(element) {
guard iteration < threshold else { break }
scrollDown()
iteration++
}
if !elementIsWithinWindow(element) { scrollDown(threshold) }
while !elementIsWithinWindow(element) {
guard iteration > 0 else { break }
scrollUp()
iteration--
}
}
This last method isn't super efficient, but it should at least enable you to find elements off screen. Of course if you know your target element is always going to be above or below your starting point in a given test you could just write a scrollDownUntil or a scrollUpUntill method without the threshold logic here.
Hope this helps!
Swift 5 Update
func elementIsWithinWindow(element: XCUIElement) -> Bool {
guard element.exists && !element.frame.isEmpty && element.isHittable else { return false }
return XCUIApplication().windows.element(boundBy: 0).frame.contains(element.frame)
}
func scrollDown(times: Int = 1) {
let mainWindow = app.windows.firstMatch
let topScreenPoint = mainWindow.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.05))
let bottomScreenPoint = mainWindow.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.90))
for _ in 0..<times {
bottomScreenPoint.press(forDuration: 0, thenDragTo: topScreenPoint)
}
}
func scrollUp(times: Int = 1) {
let mainWindow = app.windows.firstMatch
let topScreenPoint = mainWindow.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.05))
let bottomScreenPoint = mainWindow.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.90))
for _ in 0..<times {
topScreenPoint.press(forDuration: 0, thenDragTo: bottomScreenPoint)
}
}
func scrollUntilElementAppears(element: XCUIElement, threshold: Int = 10) {
var iteration = 0
while !elementIsWithinWindow(element: element) {
guard iteration < threshold else { break }
scrollDown()
iteration += 1
}
if !elementIsWithinWindow(element: element) {
scrollDown(times: threshold)
}
while !elementIsWithinWindow(element: element) {
guard iteration > 0 else { break }
scrollUp()
iteration -= 1
}
}
What i had to do to address this problem is to actually swipe up or down in my UI testing code. Have you tried this?
XCUIApplication().swipeUp()
Or you can also do WhateverUIElement.swipUp() and it will scroll up/down with respect to that element.
Hopefully they will fix the auto scroll or auto find feature so we don't have to do this manually.
You should check isHittable property.
If view is not hidden, the corresponding XCUIElement is hittable. But there is a caveat. "View 1" can be overlapped by "View 2", but the element corresponding to "View 1" can be hittable.
Since you have some XCUIElements in the bottom of the tableview (table footer view), the way of scrolling the tableview all the way to the bottom in the UI test, supposing your tableview has a lot data, is by tap().
.swipeUp() also does the job but the problem is when your test data is huge, it takes forever to swipe, as oppose to .tap() which directly jumps to the bottom of the tableView.
More specially:
XCUIElementsInTheBottomOrTableFooterView.tap()
XCTAssert(XCUIElementsInTheBottomOrTableFooterView.isHittable, "message")
Looks like this is a known bug :-(
https://forums.developer.apple.com/thread/9934