Disable swipe on TabView while editing (swiftui) - swiftui

I'm trying to disable the possibility to swipe a TabView in swiftui while a variable (Bool) is set to true but I must miss something very simple. I found an answer here as well as many other posts saying the same but when I run a test it doesn't prevent the swipe for me.
I have a simple test code:
struct TabViewTest: View {
#State var editing: Bool = false
var numbers:[Int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
var body: some View {
TabView {
ForEach(numbers, id: \.self) { index in
Button ("Hello, World \(index)!") {
editing = !editing
print("Btn \(index) changed editing to \(editing)")
}
.gesture(editing ? DragGesture() : nil)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .never))
}
}
Yet when running this I still can swipe no matter if the "editing" variable is true or false.
Here is the log from pressing the button on each Tab:
Btn 1 changed editing to true
Btn 2 changed editing to false
Btn 3 changed editing to true
Btn 4 changed editing to false
Btn 5 changed editing to true
Btn 6 changed editing to false
What am I missing? everything I find about this offer the same solution yet I'm not able to implement it ... I must miss something basic ...
I have this on the simulator with IOS 16 if this would make a difference.
Edit: it look like this is an issue others face but have not found a solution to yet, I have found a similar question here.

Your idea of using a DragGesture to prevent TabView from seeing the drag is good, but you need the DragGesture to apply to the whole screen, not just to the button.
struct TabLocker: View {
let pages = Array(1...10)
#State var locked = false
var body: some View {
TabView {
ForEach(pages, id: \.self) { page in
ZStack {
// Any Color expands to take as much room as possible,
// so it will fill the screen.
// Color.clear is invisible so it won't affect the page appearance.
// But because it's invisible, SwiftUI doesn't allow it
// to receive touches by default.
// The .contentShape(Rectangle()) allows it to receive touches.
Color.clear
.contentShape(Rectangle())
.gesture(locked ? DragGesture() : nil)
VStack {
Text("Page \(page)")
Toggle("Locked", isOn: $locked)
.fixedSize()
}
}
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
}
}

I actually found an answer in the comments of this question.
The issue is: any view that has an "onTapGesture" will ignore ".gesture(...)".
The solution is to use ".simulataneousGesture(...)" instead to ensure the gesture is capture and handled by both view/modifier.
It worked perfectly in my case after changing it. The only exception is for 2 finger drag gesture.
In my case the following code worked:
ForEach(numbers, id: \.self) { index in
Button ("Hello, World \(index)!") {
editing = !editing
print("Btn \(index) changed editing to \(editing)")
}
.simulataneousGesture(editing ? DragGesture() : nil)
}

Related

SwiftUI - How to make navigation bar title editable (without changing any other behavior)?

I really like the look of the navigation bar title in SwiftUI, and I like that it appears just below the safe area, but appears in the principal part of the toolbar when you scroll down. I'm wondering how to completely replicate this look and behavior but make it editable by the user (most likely through a textfield?)
I've tried
.toolbar {
ToolbarItem(placement: .principal) {
TextField("Navigation Title", text: $mainTitle)
}
}
But this simply places the title in the toolbar at all times, rather than only when you scroll slightly.
Any ideas?
First I explain why your code does not work:
Only the size of the navigationTitle changes when you start to scroll, not the size of the whole toolbar or its items.
But I think I have a solution:
import SwiftUI
struct ContentView: View {
#State private var title: String = "Title"
#State private var titleSmall: Bool = false
var body: some View {
NavigationView {
List {
GeometryReader { geo in
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
.onChange(of: geo.frame(in: .global).minY) { val in
if val <= 53.5 {
titleSmall = true
} else {
titleSmall = false
}
}
}
Text("Hello, world!")
}
.toolbar {
ToolbarItem(placement: .principal) {
TextField("Title", text: $title)
.multilineTextAlignment(.center)
.font(titleSmall ? .headline : .largeTitle.bold())
.accessibilityAddTraits(.isHeader)
}
}
}
}
}
What the code does is: It gets the top Y position from the first (in this example) list item.
Then it checks if the first list item is under the title bar and changes the font size of the title if necessary.
The only Problem I see is that there is a pretty rough transition between small and big title but I think you can figure out how to fix this.
If you have more questions how the code works just ask
I hope that solves your question.
And I would recommend to have a look at Paul Hudson’s video about the Geometry Reader (he’s a great YouTuber): https://youtu.be/kh9lnIYgW1E
I just realized that it says „OLD“ in the video title, so it may be outdated.
But he has some other videos about the Geometry Reader.
just search for „Paul Hudson Geometry Reader“

SwifUI onTapGesture inside ScollView not alway detect tap

I have problem with Views that have onTapGesture and are placed inside ScollView
This onTapGesture is not always reacting to tap gesture.
I need to tap precisely on such view.
It seems like there is conflict with ScrollView drag?
I've tried
highPriorityGesture
onTapGesture
gesture(DragGesture(minimumDistance:0).onChange { })
gesture(TapGesture().onEnded { })
Views have contentShape(Rectangle()) added to them
It somtimes works ok sometimes doesn't. On simulature it most of the time works ok, on physical device it is much worse.
ScrollViewReader { proxy in
HStack(spacing: spacing) {
ForEach(0 ..< elements.count, id: \.self) { i in
Text(elements[i])
.fixedSize()
.contentShape(Rectangle())
.onTapGesture {
withAnimation {
selectedElement = i
}
}
}
I couldn't reproduce the behavior that you describe with that example code, but maybe you could try the following modifier in case another gesture is operating at the same time:
.simultaneousGesture(TapGesture().onEnded({
selectedElement = 1
}))

Split view on iPad portrait mode results in a useless view being presented on startup

With the following code, Useless View is shown on app startup for iPads in portrait mode. One press of the Back button results in the detail view showing "Link 1 destination". Only upon the second press is the sidebar shown. This is not the behavior we'd want for pretty much any app.
If we remove the Useless View from the code, we get a blank screen upon startup and we still have to press the back button once to get to the Link 1 destination, and twice to get to the sidebar. Also, the styling of the list in the sidebar appears less desirable.
The behavior we want is Link 1 destination shown on app startup and a single press of the Back button bringing us to the sidebar. Is this completely standard behavior that we'd expect for any app even possible with SwiftUI 3?
struct ContentView: View {
#State var selection: Int? = 0
var body: some View {
NavigationView {
List {
NavigationLink("Link 1", tag: 0, selection: $selection) {
Text("Link 1 destination")
}
NavigationLink("Link 2", tag: 1, selection: $selection) {
Text("Link 2 destination")
}
}
//If we delete the Useless View, we still have to press Back twice
//to get to the sidebar nav.
Text("Useless View")
.padding()
}
}
}
#Yrb comment pointed to the right track with tinkering with underlying UIKit UISplitViewController to be forced to show the primary column. However, with somewhat heavy views in the Detail, there is some flashing of the primary column appearing and disappearing on slower iPads. So I did this with the Introspect library:
struct ContentView: View {
#State var selection: Int? = 0
var body: some View {
NavigationView {
List {
NavigationLink("Link 1", tag: 0, selection: $selection) {
Text("Link 1 destination")
}
NavigationLink("Link 2", tag: 1, selection: $selection) {
Text("Link 2 destination")
}
}
//In my case I never have a 'Nothing Selected' view, you could replace EmptyView() here
//with a real view if needed.
EmptyView()
}
.introspectSplitViewController { svc in
if isPortrait { //If it's done in landscape, the primary will be incorrectly disappeared
svc.show(.primary)
svc.hide(.primary)
}
}
}
}
This presents as we'd expect, with no flashing or other artifacts.

MVVM and passing over optinal value to the viewModel

I am trying to get a MVVM approach to work for my swiftui app but I have a problem where a optional variabel does not seem to get passed over correct.
So the home view you can click either on "new" or "edit", on edit I want to pass the value over, the code for this screen:
Button:
Button {
selectedTag = tag
isShowingEdit.toggle()
} label: {
Image(systemName: "pencil")
.font(Font.system(size: 30, weight: .bold))
.foregroundColor(.gray)
}
Navigationlink:
NavigationLink(destination: TagsCreatingView(viewModel: TagCreateViewModel(tag: selectedTag)), isActive: $isShowingEdit) { EmptyView() }
The TagCreateViewModel init looks as following:
init(tag: TagMO?) {
if let tag = tag {
self.tag = tag
title = tag.title
selectedColor = Int(tag.color)
}
If I do a print on selectedTag when I click the button it has the correct value, but over inte the viewModel it will be nil. Also if I click the edit button twice it works as planned and tag is not nil ( by twice I mean click edit, on the other screen click cancel then edit again..)
The problem was that inside the destination view had
#StateObject var viewModel: TagCreateViewModel
Changing it to
#ObservedObject var viewModel: TagCreateViewModel
Solved it since the ownership got put correct :9

Handling focus event changes on tvOS in SwiftUI

How do I respond to focus events on tvOS in SwiftUI?
I have the following SwiftUI view:
struct MyView: View {
var body: some View {
VStack {
Button(action: {
print("Button 1 pressed")
}) {
Text("Button 1")
}.focusable(true) { focused in
print("Button 1 focused: \(focused)")
}
Button(action: {
print("Button 2 pressed")
}) {
Text("Button 2")
}.focusable(true) { focused in
print("Button 2 focused: \(focused)")
}
}
}
Clicking either of the buttons prints out correctly. However, changing focus between the two buttons does not print anything.
This guy is doing the same thing with rows in a list & says it started working for him with the Xcode 11 GM, but I'm on 11.5 and it's definitely not working (at least not for Buttons (or Toggles - I tried those too)).
Reading the documentation, this appears to be the correct way to go about this, but it doesn't seem to actually work. Am I missing something, or is this just broken?
In case anybody else stumbles upon this question, the answer is to make your view data-focused
So, if you have a list of movies in a scrollview, you would add this to your outer view:
var myMovieList: [Movie];
#FocusState var selectedMovie: Int?;
And then somewhere in your body property:
ForEach(0..<myMovieList.count) { index in
MovieCard(myMovieList[i])
.focusable(true)
.focused($selectedMovie, equals: index)
}
.focusable() tells the OS that this element is focusable, and .focused tells it to make that view focused when the binding variable ($selected) equals the value passed to equals: ...
For example on tvOS, if you wrap this in a scrollview and press left/right, it will change the selection and update the selected index in the $selectedMovie variable; you can then use that to index into your movie list to display extra info.
Here is a more complete example that will also scale the selected view:
https://pastebin.com/jejwYxMU