├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── Tweak.xm ├── UIScroller.plist ├── build.sh └── control /.gitignore: -------------------------------------------------------------------------------- 1 | .theos/ 2 | packages/ 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Dega 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Install Destination 3 | THEOS_DEVICE_IP = localhost 4 | THEOS_DEVICE_PORT = 2222 5 | 6 | # Configurations 7 | TWEAK_NAME = UIScroller 8 | $(TWEAK_NAME)_FILES = Tweak.xm 9 | $(TWEAK_NAME)_CFLAGS = -fobjc-arc 10 | $(TWEAK_NAME)_FRAMEWORKS = UIKit 11 | 12 | ARCHS = arm64e arm64 13 | FINALPACKAGE = 1 14 | TARGET = iphone:clang:latest:14.0 15 | INSTALL_TARGET_PROCESSES = SpringBoard 16 | 17 | # build for rootful, rootless, or roothide 18 | THEOS_PACKAGE_SCHEME = roothide 19 | 20 | # Theos 21 | include $(THEOS)/makefiles/common.mk 22 | include $(THEOS_MAKE_PATH)/tweak.mk 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## UIScroller 2 | UIScroller is an intelligent iOS enhancement that transforms your scrolling experience with intuitive auto-scrolling capabilities. Perfect for reading long articles, browsing social media, or any content that requires continuous scrolling 3 | 4 | ## How to open the menu? 5 | When you are inside an app, long-press your screen with 2 fingers, the menu will show up. 6 | 7 | ## How to use? 8 | Simply scroll on any view and UIScroller will automatically continute scrolling until it's stopped either by tapping the view again or holding the screen. 9 | 10 | ## Example 11 | Open Twitter / X app, then start scrolling your feed and you'll see the tweak will continue scrolling the page automatically. 12 | -------------------------------------------------------------------------------- /Tweak.xm: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface UIScrollView (UIScroller) 4 | @property (nonatomic,readonly) UIPanGestureRecognizer *panGestureRecognizer; 5 | - (void)startUIScroller; 6 | - (void)stopUIScroller; 7 | - (void)autoScroll; 8 | - (void)handleTaps:(UITapGestureRecognizer *)gesture; 9 | @end 10 | 11 | NSTimer *timer = nil; 12 | NSTimer *autoDisableTimer = nil; 13 | BOOL verticalDown = NO; 14 | int scrollSpeedType = 0; // 0: Slow, 1: Normal, 2: Medium, 3: Fast 15 | int autoDisableMinutes = 0; // 0: Disabled, >0: Minutes until auto-disable 16 | 17 | id topViewController() { 18 | // Initialize a UIWindow instance to store the key window 19 | UIWindow *keyWindow = nil; 20 | // Get all windows of the application 21 | NSArray *windows = [[UIApplication sharedApplication] windows]; 22 | // Iterate through all windows to find the key window 23 | for (UIWindow *window in windows) { 24 | if (window.isKeyWindow) { 25 | keyWindow = window; 26 | break; 27 | } 28 | } 29 | // Get the root view controller of the key window 30 | UIViewController *rootController = keyWindow.rootViewController; 31 | // Initialize a UIViewController instance to store the top-most view controller 32 | UIViewController *topController = rootController; 33 | // Iterate to the top-most view controller that is presented 34 | while (topController.presentedViewController) topController = topController.presentedViewController; 35 | // Check if the top-most view controller is a UINavigationController 36 | // If yes, get its visible view controller 37 | if ([topController isKindOfClass:[UINavigationController class]]) { 38 | UIViewController *visibleController = ((UINavigationController *)topController).visibleViewController; 39 | if (visibleController) topController = visibleController; 40 | } 41 | // Return the top-most view controller if it is not the root controller 42 | // Otherwise, return the root controller 43 | if (topController != rootController) return topController; 44 | else return rootController; 45 | } 46 | 47 | void openSimpleMenu() { 48 | // Make sure to only show this once 49 | if (![topViewController() isKindOfClass:[UIAlertController class]]) { 50 | // Variables 51 | BOOL isDisabled = [[NSUserDefaults standardUserDefaults] boolForKey:@"uiscroller_disabled"]; 52 | // Alert Controller 53 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"UIScroller Quick Menu" 54 | message:nil 55 | preferredStyle:UIAlertControllerStyleAlert]; 56 | // Action(s) 57 | UIAlertAction *speed = [UIAlertAction actionWithTitle:[NSString stringWithFormat:@"Speed: %@", scrollSpeedType == 3 ? @"Fast" : (scrollSpeedType == 2 ? @"Medium" : (scrollSpeedType == 1 ? @"Normal" : @"Slow"))] style:UIAlertActionStyleDefault 58 | handler:^(UIAlertAction *action) { 59 | if (scrollSpeedType == 3) scrollSpeedType = 0; 60 | else scrollSpeedType += 1; 61 | }]; 62 | UIAlertAction *autoDisable = [UIAlertAction actionWithTitle:[NSString stringWithFormat:@"Auto-disable: %@", autoDisableMinutes == 0 ? @"Off" : [NSString stringWithFormat:@"%d min", autoDisableMinutes]] style:UIAlertActionStyleDefault 63 | handler:^(UIAlertAction *action) { 64 | // Show input alert 65 | UIAlertController *inputAlert = [UIAlertController alertControllerWithTitle:@"Set auto-disable Timer" 66 | message:@"Enter minutes (0 to disable)" 67 | preferredStyle:UIAlertControllerStyleAlert]; 68 | 69 | [inputAlert addTextFieldWithConfigurationHandler:^(UITextField *textField) { 70 | textField.keyboardType = UIKeyboardTypeNumberPad; 71 | textField.placeholder = @"Minutes"; 72 | textField.text = [NSString stringWithFormat:@"%d", autoDisableMinutes]; 73 | }]; 74 | 75 | UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault 76 | handler:^(UIAlertAction *action) { 77 | NSString *input = inputAlert.textFields.firstObject.text; 78 | int minutes = [input intValue]; 79 | 80 | // Validate input (max 180 minutes = 3 hours) 81 | if (minutes < 0) minutes = 0; 82 | if (minutes > 180) minutes = 180; 83 | 84 | autoDisableMinutes = minutes; 85 | 86 | // Cancel existing auto-disable timer 87 | if (autoDisableTimer) { 88 | [autoDisableTimer invalidate]; 89 | autoDisableTimer = nil; 90 | } 91 | 92 | // Show confirmation 93 | NSString *message = autoDisableMinutes == 0 ? 94 | @"Auto-disable timer disabled" : 95 | [NSString stringWithFormat:@"Auto-disable timer set to %d minutes", autoDisableMinutes]; 96 | 97 | UIAlertController *confirmation = [UIAlertController alertControllerWithTitle:@"Timer Updated" 98 | message:message 99 | preferredStyle:UIAlertControllerStyleAlert]; 100 | [confirmation addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]]; 101 | [topViewController() presentViewController:confirmation animated:YES completion:nil]; 102 | }]; 103 | 104 | UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]; 105 | 106 | [inputAlert addAction:confirmAction]; 107 | [inputAlert addAction:cancelAction]; 108 | 109 | [topViewController() presentViewController:inputAlert animated:YES completion:nil]; 110 | }]; 111 | UIAlertAction *toggle = [UIAlertAction actionWithTitle:[NSString stringWithFormat:@"%@ for this app", isDisabled ? @"Enable" : @"Disable"] style:UIAlertActionStyleDefault 112 | handler:^(UIAlertAction *action) { 113 | if (isDisabled) [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"uiscroller_disabled"]; 114 | else [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"uiscroller_disabled"]; 115 | }]; 116 | UIAlertAction *dismiss = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]; 117 | // Add Action to the Alert Controller 118 | [alert addAction:speed]; 119 | [alert addAction:autoDisable]; 120 | [alert addAction:toggle]; 121 | [alert addAction:dismiss]; 122 | // Show the alert to the most top view controller 123 | [topViewController() presentViewController:alert animated:YES completion:nil]; 124 | } 125 | } 126 | 127 | %hook UIWindow 128 | 129 | - (void)becomeKeyWindow { 130 | %orig; 131 | // Add tap gesture recognizer (menu) 132 | UILongPressGestureRecognizer *menuGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleMenuLongPress:)]; 133 | menuGestureRecognizer.numberOfTouchesRequired = 2; 134 | [self addGestureRecognizer:menuGestureRecognizer]; 135 | } 136 | 137 | %new 138 | - (void)handleMenuLongPress:(UITapGestureRecognizer *)gesture { 139 | openSimpleMenu(); 140 | } 141 | 142 | %end 143 | 144 | %hook UIScrollView 145 | 146 | - (void)didMoveToWindow { 147 | %orig; 148 | 149 | // Add tap gesture recognizer (stop) 150 | UITapGestureRecognizer *singleTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTaps:)]; 151 | singleTapGestureRecognizer.numberOfTapsRequired = 1; 152 | singleTapGestureRecognizer.cancelsTouchesInView = NO; 153 | [self addGestureRecognizer:singleTapGestureRecognizer]; 154 | 155 | } 156 | 157 | - (void)_scrollViewWillBeginDragging { 158 | %orig; 159 | 160 | CGPoint velocity = [self.panGestureRecognizer velocityInView:self]; 161 | 162 | // Only run if enabled 163 | BOOL isDisabled = [[NSUserDefaults standardUserDefaults] boolForKey:@"uiscroller_disabled"]; 164 | if (!isDisabled) { 165 | if (fabs(velocity.y) > fabs(velocity.x)) { 166 | if (velocity.y > 0) { 167 | // User scrolling down 168 | verticalDown = NO; 169 | [self startUIScroller]; 170 | } else { 171 | // User scrolling up 172 | verticalDown = YES; 173 | [self startUIScroller]; 174 | } 175 | } 176 | } 177 | 178 | } 179 | 180 | - (BOOL)_scrollViewWillEndDraggingWithDeceleration:(BOOL)arg1 { 181 | if (!%orig && !arg1) [self stopUIScroller]; 182 | return %orig; 183 | } 184 | 185 | %new 186 | - (void)handleTaps:(UITapGestureRecognizer *)gesture { 187 | // UIScroller will still scroll previous page if not stopped 188 | [self stopUIScroller]; 189 | } 190 | 191 | %new 192 | - (void)startUIScroller { 193 | // Invalidate first 194 | [self stopUIScroller]; 195 | // Then start scrolling 196 | timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(autoScroll) userInfo:nil repeats:YES]; 197 | 198 | // Set up auto-disable timer if enabled 199 | if (autoDisableMinutes > 0) { 200 | // Cancel existing auto-disable timer if any 201 | if (autoDisableTimer) { 202 | [autoDisableTimer invalidate]; 203 | autoDisableTimer = nil; 204 | } 205 | 206 | // Create new auto-disable timer 207 | autoDisableTimer = [NSTimer scheduledTimerWithTimeInterval:autoDisableMinutes * 60 208 | target:self 209 | selector:@selector(autoDisableScrolling) 210 | userInfo:nil 211 | repeats:NO]; 212 | } 213 | } 214 | 215 | %new 216 | - (void)stopUIScroller { 217 | // Invalidate 218 | [timer invalidate]; 219 | timer = nil; 220 | } 221 | 222 | %new 223 | - (void)autoScroll { 224 | 225 | // Variables 226 | float scrollSpeed = 1.0; // Default scrolling speed 227 | CGPoint offset = self.contentOffset; 228 | 229 | // Adjust based on user's prefs 230 | if (scrollSpeedType == 0) scrollSpeed = 0.5; // Slow 231 | else if (scrollSpeedType == 1) scrollSpeed = 1.0; // Normal 232 | else if (scrollSpeedType == 2) scrollSpeed = 1.5; // Medium 233 | else if (scrollSpeedType == 3) scrollSpeed = 2.0; // Fast 234 | 235 | // Condition 236 | if (verticalDown) offset.y += scrollSpeed; 237 | else offset.y -= scrollSpeed; 238 | 239 | // Stop scrolling when exceed the top or bottom 240 | float endPoint = offset.y + self.frame.size.height; 241 | if (self.contentOffset.y < 0 || endPoint >= self.contentSize.height) { 242 | [self stopUIScroller]; 243 | } 244 | 245 | // Scroll the target content offset 246 | [self setContentOffset:offset animated:NO]; 247 | } 248 | 249 | %new 250 | - (void)autoDisableScrolling { 251 | // Stop the scrolling 252 | [self stopUIScroller]; 253 | 254 | // Reset the auto-disable timer 255 | [autoDisableTimer invalidate]; 256 | autoDisableTimer = nil; 257 | 258 | // Show a notification to the user 259 | UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"UIScroller" 260 | message:@"Auto-scrolling has been automatically disabled" 261 | preferredStyle:UIAlertControllerStyleAlert]; 262 | UIAlertAction *ok = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]; 263 | [alert addAction:ok]; 264 | [topViewController() presentViewController:alert animated:YES completion:nil]; 265 | } 266 | 267 | %end 268 | 269 | %ctor { 270 | // Only run on user installed apps 271 | NSString *executablePath = NSProcessInfo.processInfo.arguments[0]; 272 | if ([executablePath containsString:@"/var/containers/Bundle/Application"]) %init; 273 | } -------------------------------------------------------------------------------- /UIScroller.plist: -------------------------------------------------------------------------------- 1 | { Filter = { Bundles = ( "com.apple.UIKit" ); }; } 2 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Colors for output 4 | GREEN='\033[0;32m' 5 | NC='\033[0m' # No Color 6 | 7 | # Comment out THEOS_PACKAGE_SCHEME in Makefile before building 8 | sed -i 's/^THEOS_PACKAGE_SCHEME/#THEOS_PACKAGE_SCHEME/' Makefile 9 | 10 | echo -e "${GREEN}Building rootful (arm) package...${NC}" 11 | make clean package 12 | 13 | echo -e "\n${GREEN}Building rootless (arm64) package...${NC}" 14 | THEOS_PACKAGE_SCHEME=rootless make clean package 15 | 16 | echo -e "\n${GREEN}Building roothide (arm64e) package...${NC}" 17 | THEOS_PACKAGE_SCHEME=roothide make clean package 18 | 19 | # Restore THEOS_PACKAGE_SCHEME in Makefile 20 | sed -i 's/^#THEOS_PACKAGE_SCHEME/THEOS_PACKAGE_SCHEME/' Makefile 21 | 22 | echo -e "\n${GREEN}All packages built successfully!${NC}" 23 | 24 | -------------------------------------------------------------------------------- /control: -------------------------------------------------------------------------------- 1 | Package: com.iamdega.uiscroller 2 | Name: UIScroller 3 | Version: 1.0.5 4 | Architecture: iphoneos-arm 5 | Description: Rest your fingers, automate scrolling any pages! 6 | Maintainer: iamdega 7 | Author: iamdega 8 | Section: Tweaks 9 | Depends: mobilesubstrate (>= 0.9.5000) 10 | Icon: https://i.postimg.cc/7Z9SYWZS/UIScroller.png 11 | --------------------------------------------------------------------------------