alternative for arrowEdge of swiftui popover for iOS - ios

Is it possible for iPadOS to control the position of the popover, to appear on the right side of the view, or under, basically the same behaviour that arrowEdge is offering for macOS.
Right now it is always placed above of the view, like in the picture below. I would like to place it on the right side of the view, where the "red" popover is placed
The code:
struct PopoverExample: View {
#State private var isShowingPopover = false
var body: some View {
Button("Show Popover") {
self.isShowingPopover = true
}
.popover(isPresented: $isShowingPopover, arrowEdge: .trailing) {
Text("Popover Content")
.padding()
}
}
}

Related

SwiftUI List Visual Bug with Keyboard and Safe Area

I'm experiencing a visual bug when using a List and TextFields in SwiftUI. After focusing on a TextField in the List and then removing the focus (I've tried various methods of doing this, like Buttons in the List rows/keyboard toolbar etc.), there is a visual bug where the black view behind the keyboard dismisses, hangs for a second over the safe area, then suddenly disappears, causing the animation not to appear smooth. Also, if you scroll to the bottom of the List and perform the same steps, after the black view lingers for a second, it then suddenly disappears again but causes the List to 'snap' down a bit and again ruins the animation.
Here is a minimal demonstration of the issue:
struct ContentView: View {
#State var text: String = ""
#FocusState private var focused: Int?
var body: some View {
List {
ForEach(0..<50) { item in
HStack {
TextField("", text: $text)
.id(item)
.focused($focused, equals: item)
.background(.white)
Button {
focused = nil
} label: {
Text("Hide")
}
}
}
}
.scrollContentBackground(.hidden)
.background(.red)
}
}
One solution is using the List in a VStack with something below the List, but this is something I'd like to avoid with my UI. Also, I could use a ScrollView, but there is then a separate issue, where after dismissing the keyboard, the extra padding under the List produced by the keyboard avoidance stays there until you try and scroll the List again. I would also like to use the native swipe actions in my actual project. Finally, ignoring the safe area of the List/ScrollView works but then this disables keyboard avoidance, which is something that I'd like to keep.
Note: experienced on iOS 16.0 and 16.1
I am not 100% sure why this is happening, since I have not worked a lot with lists. But I guess it has something to do with that you are not explicitly telling your view to ignore the safe area. Adding .edgesIgnoringSafeArea(.bottom) to your list when leaving focus, and then changing it to trailing on entering focus seems to solve it.
struct ContentView: View {
#State var text: String = ""
#State var ignoreSafeArea = false
#FocusState private var focused: Int?
var body: some View {
List {
ForEach(0..<50) { item in
HStack {
TextField("", text: $text)
.id(item)
.focused($focused, equals: item)
.background(.white)
Button {
focused = nil
} label: {
Text("Hide")
}
}
}
.onChange(of: focused) { focused in
if focused == nil {
ignoreSafeArea = true
} else {
ignoreSafeArea = false
}
}
}.edgesIgnoringSafeArea(ignoreSafeArea ? .bottom : .trailing)
}
}

Draw a view over the navigation bar

I'm working on a bottom sheet that can be invoked from any other screen. The bottom sheet will be displayed on top of a half-opaque overlay and I would like the overlay to render full screen over any other view including the navigation bar and the tab bar.
However, I can't seem to be able to figure out how to get the content of the navigation bar to be behind the overlay. Here is what a demo of my current implementation looks like. As you can see, it's possible to interact with the content of the navigation bar even though it is visually displayed behind the overlay.
Half Screen
Full Screen
Back button is still active
And here is the simplified code of my current implementation:
import SwiftUI
struct MainNavigationView: View {
var body: some View {
NavigationView {
NavigationLink(destination: AnoterView()) {
Text("Navigate to the next screen")
}
}
}
}
struct AnoterView: View {
var body: some View {
ZStack {
Color(uiColor: .red)
.edgesIgnoringSafeArea(.all)
.navigationTitle("Test")
.navigationBarTitleDisplayMode(.inline)
ViewWithOverlay()
}
}
}
struct ViewWithOverlay: View {
var body: some View {
ZStack {
// I'd like this overlay to be rendered over the navigation bar
Color(uiColor: .blue)
.edgesIgnoringSafeArea(.all)
Color(uiColor: .green)
}
}
}
And the outcome:
As you can see, while the blue color, which represent my overlay, is drawn over the red color, the title and the back button are still displayed on top of the blue color.
I understand why this is happening, but I cannot think of any workaround in SwiftUI to fix this that can be invoked from any view.
Any help is appreciated.
If you want to overlay everything then it should be on root, including over NavigationView as well, ie.
ZStack {
NavigationView {
Color(uiColor: .red).edgesIgnoringSafeArea(.all)
}
ViewWithOverlay() // << here !!
}
.edgesIgnoringSafeArea(.all)
One thing you can do is to put the NavigationView inside a ZStack. This way it will be in a lower layer hidden by the layer above. Here is the code that completely hides the NavigationBar on the tap of the button.
struct ContentView: View {
#State private var isPresented: Bool = false
var body: some View {
ZStack {
NavigationView {
Text("Hello World")
.navigationTitle("Welcome")
}
VStack {
}.frame(maxWidth: isPresented ? .infinity: 0, maxHeight: isPresented ? .infinity: 0)
.background(.green)
Button("Animate") {
withAnimation {
isPresented.toggle()
}
}
}
}
}

How to dismiss a presenting view to the root view of tab view in SwiftUI?

I'm using TabView on my home page. Let's just say I have 4 tabs.
On second tab, i can go to another view using NavigationLink and I go to another 2 views using NavigationLink. Then on the latest view, there is a button to present a view and i use .fullScreenCover (since I want to present it full screen).
In the presenting view, I add an X mark on the left side of the navigationBarItems to dismiss. I use #Environment(\.presentationMode) var presentationMode and presentationMode.wrappedValue.dismiss() to dismiss. But it only dismiss the presenting view to the previous view, while actually I want to dismiss it to the root of my view which is the 2nd tab of my TabView.
Is there a way to do this? Because I have looked up to some articles and nothing relevant especially in TabView context.
I also have a question tho:
Is it a right approach to use .fullScreenCover? Or is there another possible solution for example presenting a modal with full screen style (if there's any cause i'm not sure either).
Any suggestions will be very appreciated, thankyou in advance.
The presentationMode is one-level effect value, ie changing it you close one currently presented screen.
Thus to close many presented screens you have to implement this programmatically, like in demo below.
The possible approach is to use custom EnvironmentKey to pass it down view hierarchy w/o tight coupling of every level view (like with binding) and inject/call only at that level where needed.
Demo tested with Xcode 12.4 / iOS 14.4
struct ContentView: View {
var body: some View {
TabView {
Text("Tab1")
.tabItem { Image(systemName: "1.square") }
Tab2RootView()
.tabItem { Image(systemName: "2.square") }
}
}
}
struct Tab2RootView: View {
#State var toRoot = false
var body: some View {
NavigationView {
Tab2NoteView(level: 0)
.id(toRoot) // << reset to root !!
}
.environment(\.rewind, $toRoot) // << inject here !!
}
}
struct Tab2NoteView: View {
#Environment(\.rewind) var rewind
let level: Int
#State private var showFullScreen = false
var body: some View {
VStack {
Text(level == 0 ? "ROOT" : "Level \(level)")
NavigationLink("Go Next", destination: Tab2NoteView(level: level + 1))
Divider()
Button("Full Screen") { showFullScreen.toggle() }
.fullScreenCover(isPresented: $showFullScreen,
onDismiss: { rewind.wrappedValue.toggle() }) {
Tab2FullScreenView()
}
}
}
}
struct RewindKey: EnvironmentKey {
static let defaultValue: Binding<Bool> = .constant(false)
}
extension EnvironmentValues {
var rewind: Binding<Bool> {
get { self[RewindKey.self] }
set { self[RewindKey.self] = newValue }
}
}
struct Tab2FullScreenView: View {
#Environment(\.presentationMode) var mode
var body: some View {
Button("Close") { mode.wrappedValue.dismiss() }
}
}
You have 2 options:
With .fullScreenCover you will have a binding that results in it being presented you can pass this binding through to the content and when the user taps on x set to to false
You can use the #Environment(\.presentationMode) var presentationMode then call presentationMode.wrappedValue.dismiss() in your button body.
Edit:
If you want to unwind all the way you should make the TabView be binding based. I like to use SceneStorage for this take a look at this post then you can access this SceneStorage value anywhere in your app to respond to it but also to update and change the navigation (this also has the benefit of providing you proper state restoration!)
If you make your TabView in this way:
struct ContentView: View {
#SceneStorage("selectedTab") var selectedTab: Tab = .car
var body: some View {
TabView(selection: $selectedTab) {
CarTrips()
.tabItem {
Image(systemName: "car")
Text("Car Trips")
}.tag(Tab.car)
TramTrips()
.tabItem {
Image(systemName: "tram.fill")
Text("Tram Trips")
}.tag(Tab.tram)
AirplaneTrips()
.tabItem {
Image(systemName: "airplane")
Text("Airplane Trips")
}.tag(Tab.airplaine)
}
}
}
enum Tab: String {
case car
case tram
case airplaine
}
Then deep within your app in the place you want to change the navigation you can create a button view.
struct ViewCarButton: View {
#SceneStorage("selectedTab") var selectedTab: Tab = .car
var body: some View {
Button("A Button") {
selectedTab = .car
}
}
}
This will forced the selected tab to be the car tab.
if instead of this you do not want to change tab but rather change what the navigation view is navigated to you can use the same concept for that, NavigationLink that's a binding if this binding is created using a #SceneStorage then in your ViewCarButton you can make changes to it that will change the navigation state.

SwiftUI: How to make a hidden UITabBar display correctly on View appearing?

I have a TabView, with one of the tabs being a NavigationView. I want the tab bar to be hidden on the navigation destination view. I have achieved this, but the view only appears properly after the first rotation. How do I get it to appear properly the first time (2nd image)?
struct ContentView: View {
var rowIndexes : [Int] = [0,1,2,3,4,5,6]
var body: some View {
TabView {
NavigationView {
List {
ForEach(self.rowIndexes, id: \.self) {i in
NavigationLink(
destination: Color(.blue)
.onAppear(perform: {
Global.tabBar!.isHidden = true
})
.onDisappear(perform: {
Global.tabBar!.isHidden = false
})
) {
Text("\(i)")
}
}
}
}.tabItem {
Image(systemName: "list.number")
Text("List View")
}
NavigationView {
Text("Options View")
}.tabItem {
Image(systemName: "wrench")
Text("Options")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct Global {
static var tabBar : UITabBar?
}
extension UITabBar {
override open func didMoveToSuperview() {
super.didMoveToSuperview()
Global.tabBar = self
print("Tab Bar moved to superview")
}
}
Here's what the screen looks like after clicking on a link in the list for the first time (INCORRECT, with gap at the bottom where the tab bar would be if it wasn't hidden):
Here's what the screen looks like after rotating it to landscape, then back to portrait (CORRECT, blue View extending all the way to the bottom):
Is there a way to force the redraw, or simulate a rotation and back? I have tried various #State, #EnvironmentObect, and #ObservedObject solutions, but none work.
Adding ignore bottom safe area gives behaviour as you want.
NavigationLink(
destination: Color(.blue).edgesIgnoringSafeArea(.bottom) // << here !!
.onAppear(perform: {
MyGlobal.tabBar!.isHidden = true
Tested with Xcode 11.4 / iOS 13.4.

How to find out if view is currently selected in SwiftUI

I'd like to find out whether my view is currently selected or not.
I have a rectangle which represents a card. Once I tap the card, the card should go into the selected state and change properties like the color.
But once I tap somewhere else, the card should not be in that state anymore.
So far, I did not manage to figure this out as I tried with the onTapGesture property but with that I run into the problem that when I tap outside that card, the card does not change the state to false unless I tap the card again, which makes sense but it seems to be the wrong choice in this case.
import SwiftUI
struct CardView: View {
#State var selected = false
let rectangle = Rectangle()
#State var rectangleColor: Color = .purple
var body: some View {
GeometryReader { g in
self.rectangle
.foregroundColor(self.rectangleColor)
.frame(width: g.size.width / 2, height: g.size.width / 2)
.onTapGesture {
self.selected.toggle()
self.modifyColors()
}
}
}
func modifyColors() {
if selected {
rectangleColor = .red
} else {
rectangleColor = .purple
}
}
}
The selected state is the red color, the unselected one the purple color.
So I want that the color becomes red when I tap into the rectangle but not if I tap outside it.
It should become purple only again when I tap outside the rectangle but not inside it.
Example: Card is purple. I select it and it becomes red. When I then tap it again, it should stay red. It should become only purple (unselected) when I tap somewhere outside the card but not inside it.
View that contains this rectangle:
import SwiftUI
struct ContentView: View {
var body: some View {
CardView()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
It shouldn't matter where the view is actually - I want to know weather the space around that view is tapped. So if it is inside a modal, or the top view on a ZStack, the behavior should be the same.
The way to achieve this is by having the parent view manage both the selection and the gestures.
The CardView just takes a #Binding to isSelected and changes the colour accordingly:
struct CardView: View {
#Binding var isSelected: Bool
var body: some View {
Rectangle()
.fill(self.isSelected ? Color.red : Color.purple)
.aspectRatio(1.0, contentMode: .fit)
}
}
While the parent manages the #State and updates it using gestures.
struct ContentView: View {
#State var isCardSelected = false
var body: some View {
ZStack {
Color.white
.onTapGesture {
self.isCardSelected = false
}
CardView(isSelected: $isCardSelected)
.onTapGesture {
self.isCardSelected = true
}
}
}
}

Resources