Implementing Swipe and Scroll in Python

6 min read

Scrolling to find elements, swiping through carousels, and pulling to refresh are everyday interactions in mobile test suites. Platform differences mean Android and iOS need different approaches.

Android — scroll to element with UIAutomator

The most reliable Android scroll strategy uses UIAutomator's UiScrollable:

from appium.webdriver.common.appiumby import AppiumBy
 
def scroll_to_text(driver, text: str):
    """Scrolls until an element with the given text is visible, then returns it."""
    return driver.find_element(
        AppiumBy.ANDROID_UIAUTOMATOR,
        f'new UiScrollable(new UiSelector().scrollable(true))'
        f'.scrollIntoView(new UiSelector().text("{text}"))'
    )
 
# Usage
terms_item = scroll_to_text(driver, "Terms of Service")
terms_item.click()

For resource-id:

def scroll_to_id(driver, resource_id: str):
    return driver.find_element(
        AppiumBy.ANDROID_UIAUTOMATOR,
        f'new UiScrollable(new UiSelector().scrollable(true))'
        f'.scrollIntoView(new UiSelector().resourceId("{resource_id}"))'
    )

iOS — scroll loop with predicate

iOS doesn't have UIAutomator. Combine a swipe loop with element presence checks:

from selenium.common.exceptions import NoSuchElementException
from appium.webdriver.common.appiumby import AppiumBy
 
def scroll_to_accessibility_id(driver, accessibility_id: str, max_scrolls: int = 10):
    size = driver.get_window_size()
    x = size["width"] // 2
    start_y = int(size["height"] * 0.7)
    end_y = int(size["height"] * 0.3)
 
    for _ in range(max_scrolls):
        try:
            element = driver.find_element(AppiumBy.ACCESSIBILITY_ID, accessibility_id)
            if element.is_displayed():
                return element
        except NoSuchElementException:
            pass
 
        _swipe(driver, x, start_y, x, end_y, duration=0.5)
 
    raise NoSuchElementException(
        f"Element '{accessibility_id}' not found after {max_scrolls} scrolls"
    )
 
def _swipe(driver, sx, sy, ex, ey, duration):
    from selenium.webdriver.common.actions.action_builder import ActionBuilder
    from selenium.webdriver.common.actions.pointer_input import PointerInput
    from selenium.webdriver.common.actions import interaction
 
    finger = PointerInput(interaction.POINTER_TOUCH, "finger")
    actions = ActionBuilder(driver, mouse=finger)
    actions.pointer_action\
        .move_to_location(sx, sy)\
        .pointer_down()\
        .pause(duration)\
        .move_to_location(ex, ey)\
        .pointer_up()
    actions.perform()

Swipe left to advance a carousel, right to go back:

def swipe_carousel_left(driver, carousel_element):
    """Swipe left within the carousel bounds (advances to next slide)."""
    loc = carousel_element.location
    size = carousel_element.size
 
    start_x = loc["x"] + int(size["width"] * 0.8)
    end_x   = loc["x"] + int(size["width"] * 0.2)
    mid_y   = loc["y"] + size["height"] // 2
 
    _swipe(driver, start_x, mid_y, end_x, mid_y, duration=0.4)
 
 
def swipe_carousel_right(driver, carousel_element):
    loc = carousel_element.location
    size = carousel_element.size
 
    start_x = loc["x"] + int(size["width"] * 0.2)
    end_x   = loc["x"] + int(size["width"] * 0.8)
    mid_y   = loc["y"] + size["height"] // 2
 
    _swipe(driver, start_x, mid_y, end_x, mid_y, duration=0.4)

Constraining to the carousel's bounds prevents triggering iOS back-swipe or Android edge navigation.

Pull to refresh

def pull_to_refresh(driver):
    size = driver.get_window_size()
    x = size["width"] // 2
    # Slow downward drag — 1.5s makes it a pull gesture, not a scroll
    _swipe(driver,
           sx=x, sy=int(size["height"] * 0.25),
           ex=x, ey=int(size["height"] * 0.75),
           duration=1.5)

After calling pull_to_refresh(), wait for the refresh indicator to disappear:

LOADING_SPINNER = (AppiumBy.ACCESSIBILITY_ID, "loadingIndicator")
 
def refresh_and_wait(driver, page):
    pull_to_refresh(driver)
    page.wait_for_invisible(LOADING_SPINNER)  # waits up to 30s by default

Detecting end of list (loop guard)

When scrolling in a loop to find an element, guard against infinite scrolling by detecting that the page hasn't changed:

def scroll_to_element_or_end(driver, accessibility_id: str) -> bool:
    """Returns True if element found, False if end of list reached."""
    size = driver.get_window_size()
    x = size["width"] // 2
 
    for _ in range(15):
        try:
            el = driver.find_element(AppiumBy.ACCESSIBILITY_ID, accessibility_id)
            if el.is_displayed():
                return True
        except NoSuchElementException:
            pass
 
        before = driver.page_source
        _swipe(driver, x, int(size["height"] * 0.7), x, int(size["height"] * 0.3), 0.5)
        after = driver.page_source
 
        if before == after:
            return False  # page didn't change — we're at the bottom
 
    return False

page_source comparison is reliable but slow on complex screens — each call dumps the full element tree. Use sparingly.

Scroll to top (Android)

def scroll_to_top_android(driver):
    """Uses UIAutomator to scroll to the very top of a scrollable list."""
    driver.find_element(
        AppiumBy.ANDROID_UIAUTOMATOR,
        'new UiScrollable(new UiSelector().scrollable(true)).scrollToBeginning(5)'
    )

scrollToBeginning(5) performs up to 5 swipes toward the top.

In GestureUtils

class GestureUtils:
    def __init__(self, driver):
        self.driver = driver
 
    def scroll_to_text(self, text: str):
        return scroll_to_text(self.driver, text)
 
    def scroll_to_id(self, resource_id: str):
        return scroll_to_id(self.driver, resource_id)
 
    def swipe_carousel_left(self, element):
        swipe_carousel_left(self.driver, element)
 
    def pull_to_refresh(self):
        pull_to_refresh(self.driver)
 
    def scroll_to_top(self):
        scroll_to_top_android(self.driver)

// tip to track lessons you complete and pick up where you left off across devices.