How can I track scrolling with a ScrollView linked to a Custom PageControl - SwiftUI - swift

I want to create a Carousel with SwiftUI(without using TabView)
with a matching/linked Page Control in SwiftUI
So far I have both views and can update the pageControl view with a
#State var pagecontrolTracker updated with a DragGesture() .onChanged but it doesn't update the PageControl if I scroll fast, or sometimes doesn't update at all 😭.
If I Scroll slow tho, the Page Control does update sometimes as expected.
Is there a better way to update this faster and smoother?
I saw .updating modifier for DragGesture() but this doesn't work either
Full View:
struct ContentView: View {
#State var pagecontrolTracker: Int = 0
var body: some View {
VStack {
ScrollView(.horizontal) {
HStack {
ForEach(0...3, id: \.self) { index in
PagingRow()
.gesture(DragGesture().onChanged({ _ in
pagecontrolTracker = index
}))
}
}
}
PagingControls(pagecontrolTracker: $pagecontrolTracker)
}
.padding()
}
}
Inside Custom SwiftUI Row View
struct PagingRow: View {
var body: some View {
VStack {
HStack {
Image(systemName: "globe")
Text("Test Title")
}
.padding()
Button {
print("Test action")
} label: {
Text("Tap Me")
}
.buttonStyle(.borderedProminent)
.padding()
}
.background(Color.orange)
.frame(width: 200)
.cornerRadius(8)
}
}
Custom PageControl in SwiftUI
struct PagingControls: View {
#Binding var pagecontrolTracker: Int
var body: some View {
HStack {
ForEach(0...3, id: \.self) { pagingIndex in
Circle()
.fill(pagecontrolTracker == pagingIndex ? .orange : .black)
.frame(width: 8, height: 8)
}
}
}
}
Note: I don't want to use TabView since I want to be able to show the next upcoming card in the scrollView
A TabView would only show one card per page

Related

Dismissing a SwiftUI sheet with a lot of navigation views

I have a button that opens up a Profile & Settings view in a sheet that has additional navigation views in it.
I am aware how to dismiss the sheet, however this method seems to not work with additional navigation views, as when I'm deeper into the navigation and I tap "Done" to dismiss the sheet, it only returns me back to the previous navigation view until I go back to the main Profile & Settings view.
The view with the button:
import SwiftUI
struct TodayView: View {
#State private var showSheet = false
var body: some View {
NavigationView {
ScrollView {
VStack(alignment: .leading) {
TodayTabDateComponent()
.padding(.top, -10)
ForEach(0 ..< 32) { item in
VStack(alignment: .leading) {
Text("Title")
Text("Description")
}
.padding(.vertical)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal)
}
.navigationTitle("Today")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
showSheet = true
}, label: {
Image(systemName: "person.circle.fill")
.foregroundColor(.primary)
})
.sheet(isPresented: $showSheet) {
ProfileAndSettingsView()
}
}
}
}
}
}
struct TodayView_Previews: PreviewProvider {
static var previews: some View {
TodayView()
}
}
The Profile & Settings view:
import SwiftUI
struct ProfileAndSettingsView: View {
#Environment(\.presentationMode) var presentationMode
var body: some View {
NavigationView {
VStack {
List {
Section {
NavigationLink {
UserProfileView()
} label: {
HStack(alignment: .center) {
Image("avatar")
.resizable()
.aspectRatio(contentMode: .fit)
.clipShape(Circle())
.frame(width: 60, height: 60)
VStack(alignment: .leading) {
Text("Name Surname")
.font(.title2)
.fontWeight(.bold)
Text("Profile Settings, Feed Preferences\n& Linked Accounts")
.font(.caption)
}
}
}
.padding(.vertical, 6)
} }
.listStyle(.insetGrouped)
.navigationTitle("Profile & Settings")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("Done")
}
}
}
}
}
}
}
struct ProfileAndSettingsView_Previews: PreviewProvider {
static var previews: some View {
ProfileAndSettingsView()
}
}
I have looked into the issue but couldn't find any working solutions.
Is your issue here that you're applying the .sheet to the Button inside the Toolbar? I think you need to apply it to the NavigationView itself?
import SwiftUI
struct TodayView: View {
#State private var showSheet = false
var body: some View {
NavigationView {
....
}
.navigationTitle("Today")
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button(action: {
showSheet = true
}, label: {
Image(systemName: "person.circle.fill")
.foregroundColor(.primary)
})
}
}
.sheet(isPresented: $showSheet) {
ProfileAndSettingsView()
}
}
}
}
If you're targeting iOS15 or higher don't use presentationMode use #Environment(\.isPresented) private var isPresented instead this will perform the action that you want.
presentationMode was deprecated and replaced by isPresented and dismiss
I believe that presentationMode performs a similar action as dismiss does which according to Apple Docs (on the dismiss)
If you do this, the sheet fails to dismiss because the action applies to the environment where you declared it, which is that of the detail view, rather than the sheet. In fact, if you’ve presented the detail view in a NavigationView, the dismissal pops the detail view the navigation stack.
The dismiss action has no effect on a view that isn’t currently presented. If you need to query whether SwiftUI is currently presenting a view, read the isPresented environment value.
If you're targeting a lower iOS version you can create your own key like so
struct SheetOpen: EnvironmentKey {
static var defaultValue: Binding<Bool> = .constant(false)
}
extension EnvironmentValues {
var sheetOpen: Binding<Bool> {
get { self[SheetOpen.self] }
set { self[SheetOpen.self] = newValue }
}
}
Where you have your sheet defined you do this
.sheet(isPresented: $showSheet) {
ProfileAndSettingsView()
.environment(\.sheetOpen, $showSheet)
}
Then you can use it like any other environment variable
#Environment(\.sheetOpen) var sheetOpen
To dismiss it you simply do this sheetOpen.wrappedValue.toggle()

NavigationViews in TabView displayed incorrectly

I'm using SwiftUI and want to build a paged TabView with two NavigationView pages I can switch between horizontally. The pictures below show how it's supposed to work:
Here is my code for above example:
struct ContentView: View {
var body: some View {
TabView {
Page1()
Page2()
}
.tabViewStyle(.page)
}}
struct Page1: View {
var body: some View {
NavigationView {
ScrollView {
Rectangle()
.fill(.red)
.frame(width: 100, height: 100)
}
.navigationTitle("Page 1")
}
.navigationViewStyle(StackNavigationViewStyle())
}}
struct Page2: View {
var body: some View {
NavigationView {
ScrollView {
Rectangle()
.fill(.blue)
.frame(width: 100, height: 100)
}
.navigationTitle("Page 2")
}
.navigationViewStyle(StackNavigationViewStyle())
}}
Unless there's another way to do it is important that the NavigationViews are inside the TabView so that if I'm scrolling up the navigation bar title switches to .inline like you can see below:
Also, I use the StackNavigationViewStyle() because without it the pages aren't shown when I first open the app before I switch back and forth between them.
StackNavigationViewStyle() solves this but still the problem is that when I open the app for the first time the rectangles are being placed incorrectly right at the top of the screen. I then have to switch to the second page and back to get them positioned correctly:
Does anyone have an idea?
One solution is to use the Tab selection parameter. It is better not to use the StackNavigationViewStyle() with these embedded NavigationViews. What you may use is a selecter, which keeps track of the page you are on and a State variable storing the current page. This way, the NavigationView is in one place and different titels are given to each TabView item.
struct ContentView: View {
#State private var selectedTab = "1"
var body: some View {
NavigationView {
TabView(selection: $selectedTab) {
Page1()
.tag("1")
.navigationBarTitle("Page 1")
Page2()
.tag("2")
.navigationBarTitle("Page 2")
}
.tabViewStyle(.page)
}
}
}
struct Page1: View {
var body: some View {
ScrollView {
Rectangle()
.fill(.red)
.frame(width: 100, height: 100)
}
}}
struct Page2: View {
var body: some View {
ScrollView {
Rectangle()
.fill(.blue)
.frame(width: 100, height: 100)
}
}}
Updated
When you want the NavigationTitle to be .inline when scrolled, use the following code.
struct ContentView: View {
#State private var selectedTab = "1"
var body: some View {
NavigationView {
GeometryReader { proxy in
ScrollView(showsIndicators: false) {
TabView(selection: $selectedTab) {
Page1()
.tag("1")
.navigationBarTitle("Page 1")
Page2()
.tag("2")
.navigationBarTitle("Page 2")
}
.tabViewStyle(.page)
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.frame(height: proxy.size.height)
.ignoresSafeArea()
}
}
}
}
}
struct Page1: View {
var body: some View {
VStack {
Rectangle()
.fill(.red)
.frame(width: 200, height: 200)
}
}
}
struct Page2: View {
var body: some View {
VStack {
Rectangle()
.fill(.blue)
.frame(width: 200, height: 200)
}
}
}

is it possible get List array to load horizontally in swiftUI?

Do I need to dump using List and just load content into a Scrollview/HStack or is there a horizontal equivalent to stack? I would like to avoid having to set it up differently, but am willing todo so if there is no alternative... it just means recoding multiple other views.
current code for perspective:
import SwiftUI
import Combine
struct VideoList: View {
#Environment(\.presentationMode) private var presentationMode
#ObservedObject private(set) var viewModel: ViewModel
#State private var isRefreshing = false
var btnBack : some View { Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
HStack {
Image("Home") // set image here
.aspectRatio(contentMode: .fit)
.foregroundColor(.white)
}
}
}
var body: some View {
NavigationView {
List(viewModel.videos.sorted { $0.id > $1.id}, id: \.id) { video in
NavigationLink(
destination: VideoDetails(viewModel: VideoDetails.ViewModel(video: video))) {
VideoRow(video: video)
}
}
.onPullToRefresh(isRefreshing: $isRefreshing, perform: {
self.viewModel.fetchVideos()
})
.onReceive(viewModel.$videos, perform: { _ in
self.isRefreshing = false
})
}
.onAppear(perform: viewModel.fetchVideos)
.navigationViewStyle(StackNavigationViewStyle())
.navigationBarBackButtonHidden(true)
.navigationBarItems(leading: btnBack)
}
}
In general, List is List and it by design is vertical-only. For all horizontal case we should use ScrollView+HStack or ScrollView+LazyHStack (SwiftUI 2.0).
Anyway here is a simple demo of possible way that can be applicable in some particular cases. Prepared & tested with Xcode 12 / iOS 14.
Note: all tuning and alignments fixes are out of scope - only possibility demo.
struct TestHorizontalList: View {
let data = Array(1...20)
var body: some View {
GeometryReader { gp in
List {
ForEach(data, id: \.self) {
RowDataView(item: $0)
.rotationEffect(.init(degrees: 90)) // << rotate content back
}
}
.frame(height: gp.size.width) // initial fit in screen
.rotationEffect(.init(degrees: -90)) // << rotate List
}
}
}
struct RowDataView: View {
let item: Int
var body: some View {
RoundedRectangle(cornerRadius: 25.0).fill(Color.blue)
.frame(width: 80, height: 80)
.overlay(
Text("\(item)")
)
}
}

Why is ScollView content size not adjusting properly for this SwiftUI view?

I am using Xcode beta7 and the following is the code.
This is for a MacOs app.
here is my code:
import SwiftUI
struct ContentView: View {
#State var isClicked = false
var body: some View {
ScrollView {
Text("Click me").onTapGesture {
self.isClicked.toggle()
}.border(Color.red)
V1(isClicked: $isClicked)
}
}
}
struct V1: View {
#Binding var isClicked: Bool
var body: some View {
VStack(alignment: .leading) {
if isClicked {
ForEach(0...100, id: \.self) { index in
Text("value \(index)")
}
}
}.padding()
}
}
Run this code and click on the Click me button.
You will see that the scrollView's contnent size does not update and stay's squished.
If i try to resize the frame of the application by using the mouse to resize the screen, then instantly, the ScrollView's content size snaps to the correct size.
Do i need to do something to get the ScrollView to do this automatically (instead of me having to manually inscrease the frame of the app with the mouse?
I got the same issue recently on iOS, watchOS. The only way, I solve it was to move the content of the scrollView as a func in your struct having the ScrollView. As shown below.
struct ContentView: View {
#State var isClicked = false
var body: some View {
ScrollView {
Text("Click me").onTapGesture {
self.isClicked.toggle()
}.border(Color.red)
otherView()
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
func otherView() -> some View{
return VStack(alignment: .leading) {
if isClicked {
ForEach(0...100, id: \.self) { index in
Text("value \(index)")
}
}
}.padding()
}
}
It looks like a bug.

Why does modal view present just once in SwiftUI

I try to use "sheet" modifier to popup a modal view when a List cell was tapped. I find that it's ok in the List Cell View (Modal View could popup multi-times when cell be clicked), but in List View the modal view popups just once.
I have tried using Gesture and Button to trigger the popup, I find that once I use "sheet" modifier to trigger the Modal View, the Result is all the same.
struct ContentView: View {
var body: some View {
List {
ForEach(0..<5) { _ in
ListRow()
}
}
}
}
struct ListRow: View {
#State var showDetail: Bool = false
var body: some View {
HStack(spacing: 20.0) {
Button(action: {self.showDetail = true}) {
HStack {
Text("BigTitle")
.font(.largeTitle)
.fontWeight(.heavy)
Spacer()
Text("SubTitle")
.font(.headline)
.fontWeight(.medium)
.padding(.trailing, 10)
}
}
.sheet(isPresented: self.$showDetail) {
DetailView()
}
}
.frame(width: 320, height: 48)
.padding()
}
}
struct DetailView: View {
var body: some View {
Text("I am Detail View!")
.font(.largeTitle)
.fontWeight(.heavy)
}
}
I had hoped that the modal view could be popped multiple-times in the List View, but in reality the popup occurred just once.
Hope you reply!
Thank you!