A waterfall grid layout view for SwiftUI.

Overview

WaterfallGrid

A waterfall grid layout view for SwiftUI.

Image Demo 1

Swift Package Manager Twitter: @paololeonardi

Features

  • Irregular grid of content.
  • Columns number different per device orientation.
  • Spacing and grid padding customizable.
  • Horizontal or vertical scroll direction.
  • Items update can be animated.

Requirements

  • iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ / watchOS 6.0+
  • Xcode 11.0+
  • Swift 5.1+

Usage

Initialization

You can create a grid that displays the elements of collection by passing your collection of data and a closure that provides a view for each element in the collection. The grid transforms each element in the collection into a child view by using the supplied closure.

WaterfallGrid works with identifiable data (like SwiftUI.List). You can make your data identifiable in one of two ways: by passing along with your data a key path to a property that uniquely identifies each element, or by making your data type conform to the Identifiable protocol.

Example 1

A grid of views of type Image from a collection of data identified by a key path.

WaterfallGrid((0..<10), id: \.self) { index in
  Image("image\(index)")
    .resizable()
    .aspectRatio(contentMode: .fit)
}

Example 2

A grid of views of type RectangleView from a collection of Identifiable data.

WaterfallGrid(rectangles) { rectangle in
  RectangleView(rectangle: rectangle)
}

or, for simple cases like this, just:

WaterfallGrid(rectangles, content: RectangleView.init)

Grid Style

To customise the appearance of the grid call the gridStyle function and pass the parameters you want to customise.

Columns

WaterfallGrid(cards) { card in
  CardView(card: card)
}
.gridStyle(columns: 2)
WaterfallGrid(cards, content: CardView.init)
.gridStyle(
  columnsInPortrait: 2,
  columnsInLandscape: 3
)

Spacing and Padding

WaterfallGrid(rectangles, content: RectangleView.init)
.gridStyle(spacing: 8)
.padding(EdgeInsets(top: 16, leading: 8, bottom: 16, trailing: 8))

Animation

WaterfallGrid(rectangles, content: RectangleView.init)
.gridStyle(animation: .easeInOut(duration: 0.5))

Scroll Behaviour

Embed in ScrollView & Indicators option

ScrollView(showsIndicators: true) {
  WaterfallGrid(rectangles, content: RectangleView.init)
}

Horizontal Scroll Direction

ScrollView(.horizontal) {
  WaterfallGrid(rectangles, content: RectangleView.init)
  .scrollOptions(direction: .horizontal)
}

Animation Demo 4 Animation Demo 5

A Complete Example

ScrollView(.horizontal, showsIndicators: false) {
  WaterfallGrid(cards) { card in
    CardView(card: card)
  }
  .gridStyle(
    columnsInPortrait: 2,
    columnsInLandscape: 3,
    spacing: 8,
    animation: .easeInOut(duration: 0.5)
  )
  .scrollOptions(direction: .horizontal)
  .padding(EdgeInsets(top: 16, leading: 8, bottom: 16, trailing: 8))
}

Sample App

Explore the WaterfallGridSample app for some more detailed and interactive examples.

Animation Demo 1  Animation Demo 2  Animation Demo 3

Image Demo 3

Image Demo 2

Installation

Swift Package Manager

App dependency

select File > Swift Packages > Add Package Dependency and enter the repository URL (Adding Package Dependencies to Your App)

Package dependency

Add it as a dependency within your Package.swift manifest:

dependencies: [
  .package(url: "https://github.com/paololeonardi/WaterfallGrid.git", from: "1.0.0")
]

CocoaPods

You can install WaterfallGrid via CocoaPods by adding the following line to your Podfile:

pod 'WaterfallGrid', '~> 1.0.0'

Run the pod install command to download the library and integrate it into your Xcode project.

Migration Guides

Versioning

For the versions available, see the releases on this repository.

Contributing

Contributions are more than welcome. Please create a GitHub issue before submitting a pull request to plan and discuss implementation.

Author

Credits

WaterfallGrid was inspired by the following projects:

License

WaterfallGrid is available under the MIT license. See the LICENSE file for more info.

Comments
  • HELP WANTED - How show section in your WaterfallGrid?

    HELP WANTED - How show section in your WaterfallGrid?

    Come debbo modificare il codice sotto, per visualizzare le sezioni?

    ForEach(0..<3, id: \.self) { index in
                    Section {
                        Text("Sezione N. \(index)")
                        WaterfallGrid(self.thumbnails) { thumb in
                            ThumbnailView(thumb: thumb)
                                .onTapGesture {
                                    let index = self.thumbnails.firstIndex { thumbnails in
                                        thumbnails.id == thumb.id
                                    }
                                    self.thumbnails[index!].isSelected.toggle()
                            }
                        }
                    }
                }
    

    Tieni presente che se utilizzo il codice sotto:

    WaterfallGrid(thumbnails) { thumb in
                    ThumbnailView(thumb: thumb)
                        .onTapGesture {
                            let index = self.thumbnails.firstIndex { thumbnails in
                                thumbnails.id == thumb.id
                            }
                            self.thumbnails[index!].isSelected.toggle()
                    }
                }
    

    funziona perfettamente, come puoi vedere nello screenshot sotto. Schermata 2020-04-13 alle 18 05 39

    opened by CPiersigilli 6
  • Getting Cannot convert value of type 'some View' to closure result type '_'

    Getting Cannot convert value of type 'some View' to closure result type '_'

    struct WaterfallGallery: View {
        @ObservedObject var loader: ImagesLoader
        @State var urlPaths: [String]
        
        init(urlPaths: [String]) {
            var URLs = [URL]()
            for urlPath in urlPaths {
                //TODO: provide defualt error image
                URLs.append(URL(string: urlPath)!)
            }
            self._urlPaths = State(initialValue: urlPaths)
            self.loader = ImagesLoader(urls: URLs)
        }
        
        var body: some View {
            Group {
                if self.loader.isLoading {
                    placeHolder
                } else {
                    gallery
                }
            }
                .onAppear(perform: loader.load)
                .onDisappear(perform: loader.cancel)
        }
        
        private var gallery: some View {
                WaterfallGrid(self.loader.$images) { uiImage in
                    Image(uiImage: uiImage).resizable().aspectRatio(contentMode: .fit).cornerRadius(10)
                }
               .gridStyle(columns: 2)
        }
        
        private var placeHolder: some View {
             Text("Loading")
        }
    }
    

    I followed the example to use WaterfallGrid but keep getting Cannot convert value of type 'some View' to closure result type '_' on line:

    Image(uiImage: uiImage).resizable().aspectRatio(contentMode: .fit).cornerRadius(10)

    opened by ethanyuwang 3
  • passing array of URLs to image grid

    passing array of URLs to image grid

    Hi, your WaterfallGrid seems very nice and could fit in may App. However I wonder if you could giving me some pointers or direct help on how I can adapt your image grid example so that I can pass an array of URLs (URLs of jpg's in my Apps document directory) instead of using Assets.xcassets? Regards, Claes

    opened by ClaesClaes 3
  • [tvOS] can't scroll to see all screen

    [tvOS] can't scroll to see all screen

    Hello,

    I run it on simulator and I don't know if it's a bug but you can't scroll to see all screen on AppleTV, I think it's because images are not focusable..

    Capture d’écran 2019-11-13 à 09 51 13 enhancement good first issue sample app 
    opened by MrADOY 3
  • Invalid frame dimension (negative or non-finite)

    Invalid frame dimension (negative or non-finite)

    I am getting this warning when I run my app and go to the view that is using the WaterfallGrid.

    Screen Shot 2021-02-09 at 8 21 31 AM

    I think it's causing what than displays to disappear when I scroll up on the page. I'm not sure.

    `///:  FAVORITE ITEMS GRID
         WaterfallGrid(favoriteItems.favorites, id: \.self) { item in
            ZStack(alignment: .topTrailing) {
                FavoriteItemView(favoriteItem: item)
             } ///: ZSTACK
           } //: WATERFALL
              .gridStyle(
                 columnsInPortrait: 2,
                 columnsInLandscape: 3,
                 spacing: 10,
                 animation: .easeInOut(duration: 0.5)
              )
              .padding(EdgeInsets(top: 0, leading: 8, bottom: 15, trailing: 8))
                    .scrollOptions(direction: .vertical)
                    .onAppear(perform: loadFavorites)`
    

    I am pulling the items that display from Cloud Firestore.

    `func loadFavorites() {
        
        db.collection("favorites").getDocuments() { (querySnapshot, err) in
            if let err = err {
                print("Error getting documents: \(err)")
            } else {
                for document in querySnapshot!.documents {
    
                    let docId = document.documentID
                    let itemInfo = document.data()
                    let itemType = itemInfo["itemType"] as? String ?? "shareable"
                    let itemTitle = itemInfo["itemTitle"] as? String ?? "DaySpring"
                    let itemImage = itemInfo["itemImage"] as? String ?? "No Photo"
                    let itemAuthor = itemInfo["itemAuthor"] as? String ?? "DaySpring"
                 
                    db.collection("users").document(userId!).addSnapshotListener { documentSnapshot, error in
                        guard let userFavs = documentSnapshot else {
                            print("Error fetching document: \(error!)")
                            return
                        }
                        guard let data = userFavs.data() else {
                            print("Document data was empty.")
                            return
                        }
                        if let favorites = data["favorites"] as? [String: Any] {
                            if let favItem = favorites[docId] as? [String: Any] {
                                if let fav = favItem["itemActive"] as? Bool {
                                    if fav {
                                        if let itemDate = favItem["itemAdded"] as? Timestamp {
                                            let thisDate = itemDate.dateValue()
                                            self.favorites.append(FavoriteItem(id: docId, itemType: itemType, itemImage: itemImage, itemTitle: itemTitle, itemAuthor: itemAuthor, itemDate: thisDate))
                                            DispatchQueue.main.async {
                                                self.favorites.sort(by: { $0.itemDate > $1.itemDate })
                                            }
                                        }
    
                                    }
                                }
                            }
                        }
                    }
                    
                }
            }
        }
        
    }`
    
    opened by cbartellds 2
  • Grid missing inside scrollview

    Grid missing inside scrollview

    Hi, thanks for this library.

    I want to ask you something, When I put WaterfallGrid inside scrollview, the grid just disappears. How I can fix that?

    var body: some View {
            GeometryReader { geometry in
                ScrollView(.vertical) {
                    Text("Hello")
                    WaterfallGrid((0..<10), id: \.self) { index in
                      Image("swift_logo")
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                    }
                    .gridStyle(
                      columnsInPortrait: 2,
                      columnsInLandscape: 3,
                      spacing: 8,
                      padding: EdgeInsets(top: 16, leading: 8, bottom: 16, trailing: 8),
                      animation: .easeInOut(duration: 0.5)
                    )
                    .scrollOptions(
                      direction: .horizontal,
                      showsIndicators: true
                    )
                    Text("Hello")
                }
            }
        }
    
    Screen Shot 2020-11-28 at 02 04 04
    opened by gandarain 2
  • How to use multiple grid at the same screen

    How to use multiple grid at the same screen

    
                  Group {
                           WaterfallGrid(viewModel.categories) { category in
                              CategoryItem(category: category)
                           }
                           .gridStyle(columns: 3)
                        
                          WaterfallGrid(viewModel.products) { product in
                               ProductCell(product: product)
                          }
                          .gridStyle(columns: 2)
                       }
    

    When I use 2 difference grid in same page , Layout will have two difference scroll.

    What I need is only one scroll per screen.

    thanks

    opened by phuongphally 2
  • Images not loading in two columns

    Images not loading in two columns

    I am having an issue where when I first come to the screen the images don't go into two columns like they should. After I navigate into one of the image items and return to the initial screen the images go into two columns.

    Initial view of screen: IMG_0924

    Navigate away from screen: IMG_0925

    Navigate back to screen, images waterfall as they should: IMG_0926

    My code:

    ` struct FavoritesView: View {

    @EnvironmentObject var viewRouter: ViewRouter
    @ObservedObject var favoriteItems = FavoriteItemModel()
    
    var body: some View {
        
        HStack {
            WaterfallGrid(favoriteItems.favorites, id: \.self) { item in
                FavoriteItemView(favoriteitem: item)
    
            } // Waterfall
            .gridStyle(
                columnsInPortrait: 2,
                columnsInLandscape: 3,
                spacing: 8,
                padding: EdgeInsets(top: 16, leading: 8, bottom: 16, trailing: 8),
                animation: .easeInOut(duration: 0.5)
            )
            .navigationBarTitle("Favorites", displayMode: .inline)
            .onAppear(perform: getFavorites)
        } // HStack
        
    }
    
    func getFavorites() {
        favoriteItems.loadFavorites()
    }
    

    } `

    ` struct FavoriteItemView: View {

    let favoriteitem: FavoriteItem
    @EnvironmentObject var shareableItemVM: ShareableItemViewModel
    @State private var showShareableModal: Bool = false
    
    var body: some View {
        HStack{
            NavigationLink(destination: ShareableDetailView(shareableId: self.favoriteitem.id, categoryId: 0, categoryType: "", shareItemType: favoriteitem.itemType, showShareableModal: self.$showShareableModal)) {
                    WebImage(url: URL(string: self.favoriteitem.itemImage))
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .cornerRadius(10)
            }
            .environmentObject(self.shareableItemVM)
            .buttonStyle(PlainButtonStyle())
        }
    }
    

    } `

    I am using similar code with the WaterfallGrid in a couple other views, and I do not have this issue. The only thing I can think of is because these images are at different heights.

    Any thoughts?

    opened by cbartellds 2
  • How to disable scrolling?

    How to disable scrolling?

    Hi there!

    I was wondering if there is any way to disable the scrolling option and just have the stack show up? I'm trying to create a 2 by 2 grid.

    WaterfallGrid((0..<4), id: .self) { index in Rectangle() .foregroundColor(Color("primary")) .cornerRadius(8) .frame(height: 125) } .gridStyle( columns: 2, spacing: 24, padding: EdgeInsets(top: 20, leading: 30, bottom: 30, trailing: 30) )

    Thank you!

    opened by bobshoemaker 2
  • LongPressGesture Gestures conflict

    LongPressGesture Gestures conflict

    WaterfallGrid((0..<30), id: .self) { index in CategoryBook().gesture(LongPressGesture(minimumDuration: 0.5, maximumDistance: 1).onEnded{ v in self.previewShow = true }) }

    use LongPressGesture can't scroll

    opened by UnrealCherry 1
  • - make elements top-leading instead of centered

    - make elements top-leading instead of centered

    I want to allow elements to be top-left instead of centered if there is only one element. Is this the right approach? Should there be an style option for this?

    opened by danielgruber 1
  • Problem with pagination / Load more

    Problem with pagination / Load more

    When i add more image in my @state variable it scrolls down 🤔

    import SwiftUI
    import Alamofire
    import WaterfallGrid
    
    struct HomeScreen: View {
        
        @State var newPhotos: [HomeImage] = []
        @State var pageNumber : Int = 1
        @State var isPageRefreshing : Bool = false
        @State var didAppear = false
        
        @StateObject var homeImageVm = HomeScreenViewModel()
        
        var body: some View {
            NavigationView {
                ScrollView(.vertical, showsIndicators: false) {
                    LazyVStack {
                        WaterfallGrid(newPhotos) { item in
                            NavigationLink(destination:
                                            SelectedImage(image: SelectedImageClass(id: item.id, createdAt: item.createdAt, updatedAt: item.updatedAt, promotedAt: item.promotedAt, width: item.width, height: item.height, color: item.color, blur_hash: item.blur_hash, homeImageDescription: item.homeImageDescription, altDescription: item.altDescription, description: item.description, urls: item.urls, user: item.user, categories: item.categories))
                            ) {
                                AppNetworkImage(imageUrl: item.urls?.small ?? "")
                            }
                        }
                        .gridStyle(
                            columnsInPortrait: 2,
                            columnsInLandscape: 3,
                            spacing: 8,
                            animation: .linear(duration: 0.5)
                        )
                        .scrollOptions(direction: .vertical)
                    .padding(EdgeInsets(top: 16, leading: 8, bottom: 16, trailing: 8))
                    }
                    Button("Load More") {
                        
                        getHotPhotos(page: pageNumber)
                    }
                    .padding()
                }
                .onAppear(perform: {
                    if !didAppear {
                        getHotPhotos(page: pageNumber)
                    }
                    didAppear = true
                })
                .navigationBarTitle("Home", displayMode: .automatic)
                .navigationBarItems(
                    trailing:
                        NavigationLink(destination:
                                        SearchImageScreen()
                                      ) {
                                          Image(systemName: "magnifyingglass")
                                      }
                )
            }
        }
        
        func getHotPhotos(page:Int) {
            let parameters: [String: Any] = [
                "client_id" : AppConst.clinetid,
                "order_by": "latest",
                "page":String(page),
                "per_page":"20"
            ]
            AF.request(AppConst.baseurl+AppConst.photoUrl,method: .get,parameters: parameters).validate().responseDecodable(of: [HomeImage].self) { (response) in
                guard let data = response.value else {
                    isPageRefreshing = false
                    return
                }
                withAnimation {
                    newPhotos.append(contentsOf: data)
                    isPageRefreshing = false
                    pageNumber = pageNumber + 1
                }
            }
        }
    }
    
    struct HomeScreen_Previews: PreviewProvider {
        static var previews: some View {
            HomeScreen()
        }
    }
    

    https://user-images.githubusercontent.com/47414322/167401963-fb65a06d-ebf5-418e-8fbf-f9cf07fbaa05.mp4

    opened by girish54321 0
  • The size of the picture is not displayed properly in the waterfall flow

    The size of the picture is not displayed properly in the waterfall flow

    var coverRow: some View {
            VStack {
                if selectedTask.taskCover != nil {
                    Image(uiImage: selectedTask.tTaskCover)
                        .resizable()
                        .aspectRatio(contentMode: .fit)
                        .cornerRadius(CGFloat.bl_4.double)
                        .blendMode(colorScheme == .dark ? .screen : .normal)
                        .opacity(colorScheme == .dark ? 0.75 : 0.85)
                } else {
                    Button(action: {
                        isShowingImagePicker = true
                    }, label: {
                        RoundedRectangle(cornerRadius: CGFloat.bl_4.double)
                            .fill(Color.s_c.opacity(0.35))
                            .frame(height: 180)
                            .overlay(Image(systemName: "photo").modifier(SK_20(textColor: Color.t_t_c, weight: .regular)))
                    })
                }
            }
        }
    

    IMG_D754DABDA12D-1

    opened by wisepmlin 2
  • Nondeterministic layout bug

    Nondeterministic layout bug

    Hi, I am using the WaterfallGrid to layout thumbnails of different aspect ratios. The data (including the JPEG data for the thumbnail images) comes from a local database (using GRDB). But it is read synchronously on the main thread, and the order (and content) of the displayed items does not change. However the layout does. Sometimes it displays correctly, sometimes it does not (thumbnails overlap). See attached video where I switch back and forth between tabs of the app (not an actual TabView, the tab content is recreated from scratch each time).

    The following error/warning shows up in the Xcode console when the thumbnails end up overlapping, but not when the layout works correctly:

    Bound preference ElementPreferenceKey tried to update multiple times per frame.

    Any idea what is causing this, and how I could fix it?

    Btw, as you can see in the video, the layout always animates on initial display. Is that by design? And can that be disabled, ideally without disabling animations during changes?

    https://user-images.githubusercontent.com/368964/165420456-8e23a750-b1f7-4ace-b409-6b8bf8197a1f.mov

    opened by pocketpixels 3
  • Use Kingfisher on the waterfall grid

    Use Kingfisher on the waterfall grid

    Hi, I was making an app and I am using Kingfisher, it is to use Url instead of images, the Url becomes an image after being published. I was using the Waterfall Grid but it won't let me use the Kingfisher. How can I do it?

    opened by santipadilla23 0
  • Waterfall Grid Sample App not displaying full content in CardsGrid when 1 or 2 assets in grid

    Waterfall Grid Sample App not displaying full content in CardsGrid when 1 or 2 assets in grid

    When "cards" count equals 1 or 2, the grid does not show the images of either asset. Only the title and subtitle get displayed.

    Example when card count = 1 image2

    Example when card count = 2 image1

    Example of card count = 3 image0

    sample app 
    opened by jcicero02 3
Releases(1.0.1)
Owner
Paolo Leonardi
iOS Software Engineer
Paolo Leonardi
Building An Interactive Grid View

Building An Interactive Grid View This demo shows a few techniques on how to build a re-ordable grid view based on stock UICollectionView API. Here ar

Brian Coyner 8 Nov 14, 2022
🎛 QGrid: The missing SwiftUI collection view.

[NOTE] If you'd like to see QGrid in action, check out this demo of QDesigner (see video below). Install QDesigner: https://apps.apple.com/us/app/qdes

Q Mobile 1.5k Dec 23, 2022
Display list of Marvel comic Characters and its detail view

Marvel Universe Display list of Marvel comic Characters and its detail view Installation Dependencies in this project are provided via Xcodegen (proje

null 1 Oct 19, 2021
Use Yelp API to fetch restuarants around a location and show them in a table view

Yelp Use Yelp API to fetch restuarants around a location and show them in a table view - Infinite scrolling, Prefetching, Image Caching. Design Patter

null 0 Nov 1, 2021
Using UI Table View

News-App Table View와 Table view controller Table View : Table의 크기를 지정할 수 있다. Table View Controller: 전체의 뷰가 하나의 테이블 Table View Table view 구성요소 결정하기 어떤

Jiwon 0 Dec 9, 2021
Typed, yet Flexible Table View Controller

ConfigurableTableViewController Simple view controller that provides a way to configure a table view with multiple types of cells while keeping type s

Arek Holko 270 Oct 15, 2022
An easy-to-use UITableViewCell subclass that implements a swippable content view which exposes utility buttons (similar to iOS 7 Mail Application)

SWTableViewCell An easy-to-use UITableViewCell subclass that implements a swipeable content view which exposes utility buttons (similar to iOS 7 Mail

Christopher Wendel 7.2k Dec 31, 2022
Objective-C library for drag-n-drop of UITableViewCells in a navigation hierarchy of view controllers.

ios-dragable-table-cells Support for drag-n-drop of UITableViewCells in a navigation hierarchy of view controllers. You drag cells by tapping and hold

Anders Borum 49 Aug 23, 2022
Generic table view controller with external data processing

FlexibleTableViewController Swift library of generic table view controller with external data processing of functionality, like determine cell's reuse

Dmytro Pylypenko 9 May 20, 2018
Simple timeline view implemented by UITableViewCell

TimelineTableViewCell TimelineTableViewCell is a simple timeline view implemented by UITableViewCell. The UI design of TimelineTableViewCell is inspir

Zheng-Xiang Ke 1.3k Dec 25, 2022
A UITableView extension that enables cell insertion from the bottom of a table view.

ReverseExtension UITableView extension that enabled to insert cell from bottom of tableView. Concept It is difficult to fill a tableview content from

Taiki Suzuki 1.7k Dec 15, 2022
TableViews - Emoji Table View For iOS With Swift

TableViews Hello! This is EmojiTableView. Let me introduce you my first app when

null 0 Jan 2, 2022
SpanGrid is an enriched SwiftUI LazyVGrid that supports a number of extra features.

SpanGrid is an enriched SwiftUI LazyVGrid that supports a number of extra features.

James Sherlock 6 Dec 17, 2022
APDynamicGrid is a SwiftUI package that helps you create consistent and animatable grids.

APDynamicGrid Overview APDynamicGrid is a SwiftUI package that helps you create consistent and animatable grids. The DynamicGrid View preserves the sa

Antonio Pantaleo 29 Jul 4, 2022
This framework allows you to build Table views using UIKit with syntax similar to SwiftUI

This framework allows you to build Table views using UIKit with syntax similar to SwiftUI

Fun 60 Dec 17, 2022
The waterfall (i.e., Pinterest-like) layout for UICollectionView.

CHTCollectionViewWaterfallLayout CHTCollectionViewWaterfallLayout is a subclass of UICollectionViewLayout, and it trys to imitate UICollectionViewFlow

Nelson 4.4k Dec 24, 2022
A flexible grid layout view for SwiftUI

?? GridStack A flexible grid layout view for SwiftUI. WWDC20 Update Apple  released LazyVGrid and LazyHGrid at WWDC20. If you are fine to only suppor

Peter Minarik 620 Nov 11, 2022
A grid layout view for SwiftUI

Update July 2020 - latest SwiftUI now has built-in components to do this, which should be used instead. FlowStack FlowStack is a SwiftUI component for

John Susek 147 Nov 10, 2022
SwiftUI Grid layout with custom styles

SwiftUI Grid SwiftUI Grid view layout with custom styles. Features ZStack based layout Vertical and horizontal scrolling Supports all apple platforms

SpaceNation 928 Dec 15, 2022