SwiftUI - How to handle drop event outside of my View - ios

I'm implementing drag & drop in LazyVGrid. I'd like to call some function when drop happens outside of my View. How can I get the callback?
Here is my code:
var body: some View {
LazyVGrid(columns: ...) {
ForEach(...) { item in
createItem(...)
.onDrop(of: [.text], delegate: DropRelocateDelegate(didFinishDragging: {
draggingItem = nil
viewModel.didFinishDragging()
}))
}
}
}
As you can see I'm making some changes when drop happens. However this is only called when drop happens inside a grid. I'd like to make the exact same changes when drop happens outside of the View.

The most simple way would be to nest your LazyVGrid in a ZStack with a Rectangle(). The Rectangle() should fill the screen. You can then set the ZStack item alignment to top or whatever else you like.
struct SwiftUIView: View {
var body: some View {
ZStack(alignment: .top) {
Rectangle().foregroundColor(.clear)
LazyVGrid(columns: ...) {
ForEach(...) { item in
createItem(...)
.onDrop(of: [.text], delegate: DropRelocateDelegate(didFinishDragging: {
draggingItem = nil
viewModel.didFinishDragging()
}))
}
}
}
.onDrop(of: [.text], delegate: DropRelocateDelegate(didFinishDragging: {
draggingItem = nil
viewModel.didFinishDragging()
}))
}
}
Now in some cases, there may not be a need for putting onDrop() on the LazyVGrid items. Adding it to the ZStack should affect the entire screen. However, I included it just in case.

Related

SwiftUI - Dismissing a view, app goes back to content view instead of last view

I have 3 views within my app where clicking on a button on each view opens a new view. When button is clicked on 3rd view, I wish to dismiss 3rd view and 2nd view should appear. However I am noticing that app navigates back to first view instead of 2nd view.
Note: I have lots of elements, hence lots of code in my app. I removed all of them and adding minimal working code here with which I am still able to repro the problem.
// *** Main App***
#main
struct sample_sampleApp: App {
var body: some SwiftUI.Scene {
WindowGroup {
NavigationView {
ContentView().ignoresSafeArea()
}.navigationViewStyle(.stack)
}
}
}
// *** Content View or First View***
import SwiftUI
struct ContentView: View {
#State private var goToView2 = false
var body: some View {
VStack {
NavigationLink(destination: View2(), isActive: $goToView2) {
Button(action: { goToView2.toggle() }) {
Text("This is first view - Click to go to View 2").foregroundColor(.red).font(.title)
}
}
}
}
}
// *** View2 View***
import SwiftUI
import Combine
struct View2: View {
#ObservedObject var viewModel: View2ViewModel = View2ViewModel()
var body: some View {
VStack {
switch viewModel.state {
case .showView2:
VStack(alignment: .leading, spacing: 8) {
Button(action: { viewModel.navigateToView3() } ) {
Text("Second View - Click to go to View 3").foregroundColor(.blue).font(.title)
}
}
case .showView3:
View3()
}
}
.onAppear() {
viewModel.isViewVisible = true
viewModel.doSomething()
}
.onDisappear() {
viewModel.isViewVisible = false
}
}
}
// *** View model for view 2***
class View2ViewModel: ObservableObject {
enum View2AppState {
case showView2
case showView3
}
// UI changes when state gets updated.
#Published var state: View2AppState = .showView2
var isViewVisible = false
func doSomething() {
self.state = .showView2
}
func navigateToView3() {
self.state = .showView3
}
}
// *** View3***
struct View3: View {
#ObservedObject var viewModel: View3ViewModel = View3ViewModel()
#Environment(\.dismiss) var dismiss
var body: some View {
VStack {
switch viewModel.state {
case .showView3:
VStack(alignment: .leading, spacing: 8) {
Button(action: { dismiss() } ) {
Text("Third View - Click to dismiss this and to go back to view 2").foregroundColor(.green).font(.title)
}
}
}
}
.onAppear() {
viewModel.isViewVisible = true
viewModel.doSomething()
}
.onDisappear() {
viewModel.isViewVisible = false
}.navigationBarBackButtonHidden(true)
}
}
// *** View model for view 3***
class View3ViewModel: ObservableObject {
enum View3AppState {
case showView3
}
// UI changes when state gets updated.
#Published var state: View3AppState = .showView3
var isViewVisible = false
func doSomething() {
self.state = .showView3
}
}
Not sure what am I doing wrong. Sometime back I did use dismiss() while dismissing sheet and it worked fine, but not this this case. I am running this code on iOS 16.0, however my test app is set to iOS 15 as minimum version.
Edit: I tested on iOS 15.0 as well and was able to repro on it too, so something must be wrong with my code then. Not able to figure out what. I am using NavigationView in and navigation view style as Stack.
When button is clicked on 3rd view, I wish to dismiss 3rd view and 2nd view should appear.
Let's first have a look at the code of view2.
struct View2: View {
#ObservedObject var viewModel: View2ViewModel = View2ViewModel()
var body: some View {
VStack {
switch viewModel.state {
case .showView2:
VStack(alignment: .leading, spacing: 8) {
Button(action: { viewModel.navigateToView3() } ) {
Text("Second View - Click to go to View 3").foregroundColor(.blue).font(.title)
}
}
case .showView3:
View3()
}
}
}
}
// here viewModel.navigateToView3() is just changing this state
// func navigateToView3() {
// self.state = .showView3
// }
The current code behavior, when tapping to navigate to view3, replaces the content of view2 with view3 instead of actually navigating to it.
Therefore, when the dismiss function is called, it should not return to view2 as it is already in view2 displaying the content of view3.
So, going back to view1 on the dismiss press is actually the correct behavior as per the code.
If you desire the outcome you are asking, consider modifying the code using a closure passed into the child view to change the state in view2 or explore this answer to actually navigate to it.

SwiftUI view in list, detect when fully visible?

I have this simple view:
struct ContentView: View {
let items = [1,2,3,4,5,6,7,8,9,10,11,12,13,14]
var body: some View {
List {
ForEach(items, id:\.self) { i in
Text("item \(i)")
.frame(height:200)
.onAppear {
print("A: \(i)")
}
.onDisappear {
print("D: \(i)")
}
}
}
}
}
I would like to know when the cell's rect is fully visible. As is, onAppear triggers as soon as the cell comes in the view as I scroll. However, I'd like to get a hook that tells me when the cell is fully visible.
How could I do that in SwiftUI?

on change for a button to present a list?

can we use on change for a button to present a list within the same view ? I am a beginner
struct ViewMe: View {
var body: some View {
Button (action:{
},label:{
Text("Search")
})
// can we do on change here to appear a list
}
}
According to the information you gave me, here is some code that should work with explanations for each part:
struct ContentView: View {
//Your variable. #State makes it reload the view when changed.
#State var listIsShowing = false
var body: some View {
VStack {
//Your Button
Button (action:{
//Sets variable to true, showing list
listIsShowing = true
},label:{
Text("Search")
})
//To put the button at the top
Spacer(minLength: 0)
//if variable that the button changes = true, show list
if listIsShowing {
//Your list
List {
Text("Your")
Text("list")
Text("appears")
Text("When")
Text("you")
Text("click")
Text("the")
Text("button")
}
//Use below code if you want background to match the top section
//.listStyle(.plain)
}
}
}
}

SwiftUI NavigationBar not disappearing while scrolling

I want to hide my NavigationBar while scrolling, actually It must hide automatically but when I tried with multiple views It doesn't work. Also, It works when I remove custom views and capsulate List with NavigationView. But I need SearchBar and StatusView view. Is there any suggestion?
By the way, I run it on the device, I use canvas here for demonstration purposes.
Thank you.
var body: some View {
NavigationView {
VStack(spacing: 0) {
SearchBar(searchText: $viewModel.searchText)
StatusView(status: $viewModel.status)
Divider()
List(0...viewModel.characters.results.count, id: \.self) { index in
if index == self.viewModel.characters.results.count {
LastCell(vm: self.viewModel)
} else {
ZStack {
NavigationLink(destination: DetailView(detail: self.viewModel.characters.results[index])) {
EmptyView()
}.hidden()
CharacterCell(character: self.viewModel.characters.results[index])
}
}
}
.navigationBarTitle("Characters", displayMode: .large)
}
}
.onAppear {
self.viewModel.getCharacters()
}
}
Just idea, scratchy... try to put your custom views inside List as below (I know it will work, but I'm not sure if autohiding will work)
NavigationView {
List {
SearchBar(searchText: $viewModel.searchText)
StatusView(status: $viewModel.status)
Divider()
ForEach (0...viewModel.characters.results.count, id: \.self) { index in
...
Based on Asperi's solution, I wanted to have the SearchBar and StatusView always visible, i.e. it should stop scrolling after the title has disappeard. You can achieve this with a section header like shown below (just a rough sketch):
NavigationView {
List {
Section(header: {
VStack {
SearchBar...
StatusView....
}
}) {
ForEach...
}
}
}

SwiftUI add subview dynamically but the animation doesn't work

I would like to create a view in SwiftUI that add a subview dynamically and with animation.
struct ContentView : View {
#State private var isButtonVisible = false
var body: some View {
VStack {
Toggle(isOn: $isButtonVisible.animation()) {
Text("add view button")
}
if isButtonVisible {
AnyView(DetailView())
.transition(.move(edge: .trailing))
.animation(Animation.linear(duration: 2))
}else{
AnyView(Text("test"))
}
}
}
}
The above code works fine with the animation . however when i move the view selection part into a function, the animation is not working anymore (since i want to add different views dynamically, therefore, I put the logic in a function.)
struct ContentView : View {
#State private var isButtonVisible = false
var body: some View {
VStack {
Toggle(isOn: $isButtonVisible.animation()) {
Text("add view button")
}
subView().transition(.move(edge: .trailing))
.animation(Animation.linear(duration: 2))
}
func subView() -> some View {
if isButtonVisible {
return AnyView(DetailView())
}else{
return AnyView(Text("test"))
}
}
}
it looks totally the same to me, however, i don't understand why they have different result. Could somebody explain me why? and any better solutions? thanks alot!
Here's your code, modified so that it works:
struct ContentView : View {
#State private var isButtonVisible = false
var body: some View {
VStack {
Toggle(isOn: $isButtonVisible.animation()) {
Text("add view button")
}
subView()
.transition(.move(edge: .trailing))
.animation(Animation.linear(duration: 2))
}
}
func subView() -> some View {
Group {
if isButtonVisible {
DetailView()
} else {
Text("test")
}
}
}
}
Note two things:
Your two examples above are different, which is why you get different results. The first applies a transition and animation to a DetailView, then type-erases it with AnyView. The second type-erases a DetailView with AnyView, then applies a transition and animation.
Rather that using AnyView and type-erasure, I prefer to encapsulate the conditional logic inside of a Group view. Then the type you return is Group, which will animate properly.
If you wanted different animations on the two possibilities for your subview, you can now apply them directly to DetailView() or Text("test").
Update
The Group method will only work with if, elseif, and else statements. If you want to use a switch, you will have to wrap each branch in AnyView(). However, this breaks transitions/animations. Using switch and setting custom animations is currently not possible.
I was able to get it to work with a switch statement by wrapping the function that returns an AnyView in a VStack. I also had to give the AnyView an .id so SwiftUI can know when it changes. This is on Xcode 11.3 and iOS 13.3
struct EnumView: View {
#ObservedObject var viewModel: ViewModel
var body: some View {
VStack {
view(for: viewModel.viewState)
.id(viewModel.viewState)
.transition(.opacity)
}
}
func view(for viewState: ViewModel.ViewState) -> AnyView {
switch viewState {
case .loading:
return AnyView(LoadingStateView(viewModel: self.viewModel))
case .dataLoaded:
return AnyView(LoadedStateView(viewModel: self.viewModel))
case let .error(error):
return AnyView(ErrorView(error: error, viewModel: viewModel))
}
}
}
Also for my example in the ViewModel I need to wrap the viewState changes in a withAnimation block
withAnimation {
self.viewState = .loading
}
In iOS 14 they added the possibility to use if let and switch statements in function builders. Maybe it helps for your issues:
https://www.hackingwithswift.com/articles/221/whats-new-in-swiftui-for-ios-14 (at the article's bottom)

Resources