3

I'm trying to make a bidirectional TabView (with .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))) whose datasource will change over time.

Below is the code that describes what is expected in the result:

class TabsViewModel: ObservableObject {
    @Published var items = [-2, -1, 0, 1, 2]
    @Published var selectedItem = 2 {
        didSet {
            if let index = items.firstIndex(of: selectedItem), index >= items.count - 2 {
                items = items + [items.last! + 1]
                items.removeFirst()
            }
            
            if let index = items.firstIndex(of: selectedItem), index <= 1 {
                items = [items.first! - 1] + items
                items.removeLast()
            }
        }
    }
}

struct TabsView: View {
    @StateObject var vm = TabsViewModel()
    
    var body: some View {
        TabView(selection: $vm.selectedItem) {
            ForEach(vm.items, id: \.self) { item in
                Text(item.description)
                    .frame(minWidth: 0,
                           maxWidth: .infinity,
                           minHeight: 0,
                           maxHeight: .infinity,
                           alignment: .topLeading
                    )
                    .background(Color(hue: .random(in: 0...0.99), saturation: .random(in: 0...0.99), brightness: 0.5))
            }
        }
        .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
    }
}

In this case, when you try to go to next or previous page, the animation breaks off and the original page instantly returns to its place.

Is it possible to implement this exactly with TabView, without help of UIKit and third-party frameworks?

My guess is one of the possible solutions is to change the data source when the transition animation finishes, but I couldn't find a way to do this in SwiftUI.
Is my guess correct? Perhaps there are other more elegant ways to implement this functionality, and I'm coming from the wrong side.

Asio Otus
  • 61
  • 5
  • SwiftUI is made up of a lot of out of the box `View`s if you want to customize it, it is better to go with one of the UIKit forms of the same `View`. Apple has SwiftUI tutorials where they walk you how to make something similar to what you are looking for. https://developer.apple.com/tutorials/swiftui/interfacing-with-uikit – lorem ipsum Dec 26 '20 at 16:50
  • This might help you: [How can I implement PageView in SwiftUI?](https://stackoverflow.com/q/58388071/8697793) – pawello2222 Dec 26 '20 at 17:19
  • lorem ipsum, pawelio2222, thank you for your comments. I already tried to implemented examples from this links (and I failed, will try again later). In the end I managed to implement it with SwiftUIPager framework. Now I'm wondering if it is possible to implement described task with "pure" TabView. – Asio Otus Dec 26 '20 at 19:58

1 Answers1

3

The task itself can be solved using the SwiftUIPager framework.

import SwiftUI
import SwiftUIPager

class PagerConteinerViewModel: ObservableObject {
    @Published var items = [-3, -2, -1, 0, 1, 2, 3]
    @Published var selectedItemIndex = 3
    
    func updateItems (_ direction: Double) {
        withAnimation {
            if direction > 0 && selectedItemIndex >= items.count - 3 {
                items = items + [items.last! + 1]
                items.removeFirst()
            }
            
            if direction < 0 && selectedItemIndex <= 2 {
                items = [items.first! - 1] + items
                items.removeLast()
            }
        }
    }
}

struct PagerContainerView: View {
    @StateObject var vm = PagerConteinerViewModel()
    
    var body: some View {
        Pager(page: $vm.selectedItemIndex, data: vm.items,  id: \.self) { item in
            Text("\(item)")
                .frame(minWidth: 0,
                       maxWidth: .infinity,
                       minHeight: 0,
                       maxHeight: .infinity,
                       alignment: .topLeading
                )
                .background(Color(hue: .random(in: 0...0.99), saturation: .random(in: 0...0.99), brightness: 0.5))
        }
        .onDraggingEnded(vm.updateItems)
    }
}

The documentation uses onPageChanged to update the data source, but then inserting a value in the middle or beginning of the array will crash the application.

Asio Otus
  • 61
  • 5