Proper way to overlay one struct view onto another view - ios

So I have two struct views located in different files. The first view is like a home screen. The second view is like a chat dialogue box. There is a button on the first home screen view that when it gets clicked I want the chat box view to appear on top of the home screen view.
Pictures:
Home screen view + Issue discussion button that when clicked it will open chat box view
Issue discussion button clicked and is showing chat box view on top of home screen view
//Home Screen View
var body: some View {
ZStack (alignment: .top){
Button(action: {
self.viewModel.toggleChatBox()
}, label: {
Text("Issue Discussion")
Image(systemName: "message")
})
// ChatBox view that displays the chat box on the home screen
ChatBox(tailNum: viewModel._781formInstance.tailId, issueNum: viewModel._781formInstance.issueNum, Form781aInstance: self)
.background(Color(UIColor.white))
.border(.black, width: 0.7)
.frame(width: 270)
.offset(x: self.viewModel.chatBox ? 0 : UIScreen.main.bounds.width)
}
}
*I've omitted a lot of extra form the home screen view that I believe wasn't needed for this question, so let me know If you have questions or need more info
I need the ChatBox view to appear and disappear when the button is clicked. I'm currently just setting the offset based on a toggle chat box boolean var. When the var is true it will set the offset to the correct location on the screen. When false it will set the offset to be off the screen (so you cant see it). I'm doing this because for some reason Apple decided to make the .hidden() method be unconditional...
My issue is I'm getting weird behavior with the send and text box of the ChatBox. When I test it putting the chatbox in a navigation link then it behaves correctly but the navigation link takes the user to a new screen rather than displaying the chatbox over the home screen.
I'm wondering the correct way to display the chatbox view onto the home screen view

Wrapping everything in an if clause should do the trick. As long as the var that stores the state is in a #Published wrapper and your viewModel is observed:
if viewModel.whatEverYourBoolIsCalled {
ChatBox(tailNum: viewModel._781formInstance.tailId, issueNum: viewModel._781formInstance.issueNum, Form781aInstance: self)
.background(Color(UIColor.white))
.border(.black, width: 0.7)
.frame(width: 270)
.offset(x: self.viewModel.chatBox ? 0 : UIScreen.main.bounds.width)
.zIndex(3000)
}

Related

SwiftUI InputAccessoryView

I'm trying to build a chat view in SwiftUI and I want to append my input views to the keyboard, so that when I dismiss the keyboard by dragging my view gets moved with it.
When I was using UIKit I overwrote the inputAccessoryView of the ViewController. Is something similar possible with SwiftUI?
EDIT:
I already saw that I can add a UIKit TextField and add a InputAccessory for this text field. However that's not what I want to do. I want to have a global inputAccessoryView in my SwiftUI View and add my custom input view as a subview, so that it is always Visible and not an addition to my TextField.
I see two possible solutions to the behavior you want.
In some cases, SwiftUI views move out of the way of the keyboard automatically
in iOS 15 and later you can create an InputAccessoryView in Swiftui
1: In swiftUI, there are several safe areas which views lay themselves inside of by default. One of these is the keyboard safe area. This areas takes up the full screen of the device when the keyboard is hidden but shrinks to the non keyboard area of the screen when the keyboard is displayed. So in the example code below, the text field should move above the keyboard when it appears and drop down when the keyboard disappears (this does not work on an iPad when the keyboard is in the smaller floating mode).
VStack {
ScrollView {
ForEach(0 ..< 50) { item in
Text("Demo Text")
.frame(maxWidth: .infinity)
}
}
TextField("Enter Text", text: $messageText)
}
2: In iOS 15+, you can create a toolbar in the keyboard location. This essentially acts as an InputAccessoryView does in UIKit. The difference between this and method 1 is that a view in here will only appear when the keyboard is displayed. The one expiation to this is when a wired or wireless keyboard is attached to the iPhone or iPad, the toolbar view will still be displayed just at the bottom of the screen.
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Text("Apears at top of keyboard")
}
}
So putting 1 and 2 together, here is an example that implements both. You can run it in Xcode to help understand how both methods behave
VStack {
ScrollView {
ForEach(0 ..< 50) { item in
Text("Demo Text")
.frame(maxWidth: .infinity)
}
}
TextField("Enter Text", text: $messageText)
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Text("Apears at top of keyboard")
}
}

How to Programmatically Navigate List by Row Content?

I have a List within a NavigationView where each view under List should have navigatable elements attached to it (cover image, user avatar + name, etc.) For example, clicking the cover image navigates to view A, while clicking the user's name/avatar navigates to view B. Sadly, in all cases, the entire list element was clickable and did not grant the intended behavior.
At first, I tried wrapping my content within a NavigationLink.
NavigationLink(destination: Text("Media"), tag: .media, selection: $selection) {
WebImage(url: URL(string: activity.media?.coverImage?.extraLarge ?? ""))
.resizable()
.placeholder { color }
.cornerRadius(8)
.frame(width: 90, height: 135)
}
This causes an arrow to appear to indicate the view is navigatable for the user but is unwanted in this situation. It was also taking up a lot of space from the view unnecessarily.
My next attempt was to wrap the view and NavigationLink in a ZStack.
ZStack {
NavigationLink(destination: Text("Media"), tag: .media, selection: $selection) {
EmptyView()
}.hidden()
WebImage(url: URL(string: activity.media?.coverImage?.extraLarge ?? ""))
.resizable()
.placeholder { color }
.cornerRadius(8)
}.frame(width: 90, height: 135)
The .hidden() modifier was applied to the NavigationLink to prevent the arrow from appearing when the image was transparent. While this solution both hides the arrow and cleans up the extra space, there are two issues:
The entire list element is still clickable.
A ZStack covered by the .frame modifier requires I know how large I want to make it. The user's name & avatar view can't easily overcome this dilemma.
Thirdly, I tried wrapping the view in a Button where the label was the cover image and the action was to change selection to navigate programmatically, but this brought the spacing issue from #1 and the overall issue of the list element being clickable.
I later discovered a solution that would cut down the previous issues I had, but brought one problem. To understand it, this is what my main activity view looks like:
NavigationView {
List(viewModel.activities) { activity in
ActivitySelectionView(activity: activity, selection: $selection)
}.navigationTitle("Activity Feed")
}.onAppear {
viewModel.fetchActivities()
}
By encapsulating List(...) {...} in a ScrollView and changing List to a ForEach, I was able to produce the output I wanted: clickable view within an element, the cover image became lighter when clicking on it, opposed to the list element becoming darker as a whole until let go, etc.
However, this is not a list. It does not look good, nor will it look better on other platforms (this is an iOS project). For example, this code does not respect the edges as a list does. It also does not include a divider, but the Divider struct can help. I feel this is not the right solution to this problem.
To sum it all up, how do I create a List inside a NavigationView where the list respects what views inside an element are navigatable?
I found an elegant solution to my problem, so I'd like to share it for people who may stumble upon this question in the future.
You need to use a ScrollView within the List {...} somewhere. In the ScrollView block, it's perfectly suitable to make certain elements in the list cell navigatable.
NavigationView {
List(1..<11) { num in
ScrollView {
Text("\(num)!")
NavigationLink(destination: Text("Number: \(num)")) {
Text("Click me")
}
}
}
}

Unable to present ActionSheet via a NavigationBarItem in SwiftUI on an iPad

First, I have looked at a similar question, but it does not address my use case.
Present ActionSheet in SwiftUI on iPad
My issue is that I have a NavigationBarItem in my NavigationView that will toggle an ActionSheet when pressed. This behavior works properly when used on an iPhone.
However, when I use this on an iPad, both buttons on my screen will gray out and nothing happens. Clicking the buttons again will make them active (blue), but again, no sheet is presented.
Finally, if I select the button in the middle of the screen (Show Button), then an ActionSheet is properly presented on an iPad.
I have tested with Xcode 11 & iOS 13.5 and Xcode 12 & iOS 14. There is no change in behavior.
import SwiftUI
struct ContentView: View {
#State private var isButtonSheetPresented = false
#State private var isNavButtonSheetPresented = false
var body: some View {
NavigationView {
Button(action: {
// Works on iPad & iPhone
self.isButtonSheetPresented.toggle()
}) {
Text("Show Button")
}
.actionSheet(isPresented: $isButtonSheetPresented,
content: {
ActionSheet(title: Text("ActionSheet"))
})
.navigationBarTitle(Text("Title"),
displayMode: .inline)
.navigationBarItems(trailing:
Button(action: {
// Works on iPhone, fails on iPad
self.isNavButtonSheetPresented.toggle()
}) {
Text("Show Nav")
}
.actionSheet(isPresented: $isNavButtonSheetPresented,
content: {
ActionSheet(title: Text("ActionSheet"))
})
)
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
Finally, this is how it appears on an iPad when clicking on "Show Nav":
This is a simplified setup for the screen where this issue occurs. I will need to retain the navigation settings shown, but have included them for clarity.
*** UPDATED ***
While it is not possible for the real app behind this, I did remove the .navigationViewStyle(StackNavigationViewStyle()) setting, which did make an ActionSheet appear, although in the wrong spot as seen below.
This also results in bizarre placement for the Button one accessed via "Show Button".
Yes, it is a bug, but probably different - that Apple does not allow to change anchor and direction of shown ActionSheet, because it is shown, but always to the right of originated control on iPad. To prove this it is enough to change location of button in Navigation
Here is example of placing at .leading position. Tested with Xcode 12 / iOS 14
.navigationBarItems(leading:
Button(action: {
// Works on iPhone, fails on iPad
self.isNavButtonSheetPresented.toggle()
}) {
Text("Show Nav")
}
.actionSheet(isPresented: $isNavButtonSheetPresented,
content: {
ActionSheet(title: Text("ActionSheet"))
})
)
Note: SwiftUI 2.0 .toolbar behaves in the same way, ie. has same bug.
This is an old question but if someone is interested in a turnaround that works on iOS 14:
I have two navigation bar trailing buttons inside .toolbar() and they should open action sheets. I placed an invisible "bar" at the top of the view to use it as an anchor:
var body: some View {
VStack {
HStack {
Spacer()
Color.clear.frame(width: 1, height: 1, alignment: .center)
.actionSheet(/*ActionSheet for first button*/)
Spacer().frame(width: 40)
Color.clear.frame(width: 1, height: 1, alignment: .center)
.actionSheet(/*ActionSheet for second button*/)
Spacer().frame(width: 40)
}.frame(height: 1)
}
}
Cons:
There's a tiny bar/extra space at the top, noticeable especially during scrolling (Maybe putting the Stack in the background with a Stack could remove it?).
You might need to adjust the Spacers' width to try and align the ActionSheets to their respective button.
You can't force the action sheet arrows to always point upwards, I tested this on another simulator and the rightmost ActionSheet had its arrow pointing to the right (the 'illusion' that it came from the button was still there)
Here's how it looks

In SwiftUI, how can I perform a gesture but forward gestures to the view behind it?

I am creating a tooltip system.
I want to dismiss the tooltip if the user touches anywhere outside the tooltip.
I would like it so that a touch outside the tooltip both dismisses the tooltip and activates any controls the user tapped on. (So you could have a tooltip open and still click a button outside the tooltip and have it activate on the first tap.)
To do this, I have an invisible view handling the tap gesture and dismissing the tooltip, but I do not know how to make SwiftUI not intercept and cancel the tap gestures. On the web, it's the equivalent of not calling event.stopPropagation() and event.preventDefault(), or calling super in touchesBegan: in UIKit.
Any ideas?
Here is a demo of possible approach. Tested with Xcode 11.4 / iOS 13.4
struct ContentView: View {
var body: some View {
VStack {
Button("Button") { print("> button tapped")}
}
.frame(width: 200, height: 200)
.contentShape(Rectangle()) // makes all area tappable
.simultaneousGesture(TapGesture().onEnded({
print(">>> tooltip area here")
}))
.border(Color.red) // just for demo show area
}
}
You need to use this modifier:
.allowsHitTesting(false)

SwiftUI: How can I restrict the tappable area of a view when presenting a modal(actually not modal) view over a main view?

I am developing an app based on a Tabview with three TabItems. Each TabItem is a List and I would be able to show a kind of modal view over those Lists. The problem becomes when I can not call a Sheet as modal view because Sheets are almost full windowed. I need some kind of bottom modal view, so I create a View that I present over a List with higher ZIndex. It seems to work until you click in the tabbar and select another TabItem having deployed the "modal" view. The error is:
[TableView] Warning once only: UITableView was told to layout its
visible cells and other contents without being in the view hierarchy
(the table view or one of its superviews has not been added to a
window). This may cause bugs by forcing views inside the table view to
load and perform layout without accurate information (e.g. table view
bounds, trait collection, layout margins, safe area insets, etc), and
will also cause unnecessary performance overhead due to extra layout
passes.
So, I would like as solution to restrict the tappable area to the "modal" view area. ¿Is there a way to achieve this?
Probably you have some condition state depending on which you present your "modal-like" view, so depending on the same condition you can disable below TabView, like below
TabView {
// ... tabs content here
}.disabled(showingModal)
Update: Here is a demo of approach that I meant (tested with Xcode 11.3+)
struct TestTabViewModal: View {
#State private var selectedTab = 0
#State private var modalShown = false
var body: some View {
ZStack {
TabView(selection: $selectedTab) {
VStack {
Button("Show Modal") { self.modalShown = true }
.padding(.top, 40)
Spacer()
}
.tabItem {
Image(systemName: "1.circle")
}.tag(0)
Text("2").tabItem {
Image(systemName: "1.circle")
}.tag(1)
}.disabled(modalShown)
if modalShown {
RoundedRectangle(cornerRadius: 10)
.fill(Color.yellow)
.frame(width: 320, height: 240)
.overlay(Button("CloseMe") { self.modalShown = false })
}
}
}
}

Resources