What is wrong with the iOS Keyboard toolbar in SwiftUI - ios

I am aware I asked a similar question before, but it seems like I have not understood the core concept of how to present a custom toolbar above a keyboard.
I successfully solved my problem on how to present one with a search field (SwiftUI 2.0: Custom keyboard elements).
Now I want to present a keyboard when a textfield within a detail view of a list is clicked, but again the keyboard toolbar does not show. Does anyone have an idea why?
VStack {
Text("Weight:")
TextField("0", text: $weight)
.keyboardType(.decimalPad)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
HStack {
Button(action: {
print("Set bodyweight")
},
label: {Text("Bodyweight")
})
Picker("", selection: $weightType) {
ForEach(weightSuffix.allCases, id: \.self) {
Text($0.rawValue)
}
}
.pickerStyle(.segmented)
}
}
}
}
.border(Color.red)
[EDIT]
After AsperiĀ“s comment I Created a small git: https://gist.github.com/joni8a/bc021ef597cb6efa1ab0ca277d602478
Now it gets even weirder, if I attach the toolbar modifier to the list element I get the intended behavior, showing 1 button above the toolbar
If I append the toolbar modifier to the textfield inside the detail view I get the following result:
I think this is a weird behavior. It seems like I have not understood a core concept of SwiftUI. On the other hand if I can't attach the viewmodifer to the textfield itself, it is hard to uncouple the detail view from the list view ...

Related

Bug with NavigationLink and #Binding properties causing unintended Interactions in the app

I've encountered a bug in SwiftUI that could cause unintended interaction with the app without the user's knowledge.
Description
The problem seems to be related to using #Binding properties on the View structs when used in conjunction with NavigationStack and NavigationLink. If you use NavigationView with NavigationLink to display a DetailView that accepts a $Binding parameter, and that parameter is used in some sort of condition in the DetailView, it will result in unexpected behavior.
To clearly show the problem, I'm using a DetailView where the "Blue" or "Red" view is shown depending on the #Binding property. Each of those views has a .onTapGesture() modifier that prints some text when tapped. The problem is that if the Red view is shown, it detects and triggers the action on the Blue view, which could lead to unintended changes in many apps without the user's knowledge.
Replication of the problem
You can easily copy and paste this code into your own file to replicate the bug. To see the unexpected behavior, run the code below and follow these steps on the simulator:
Tap on the DetailView in the NavigationLink.
Tap the blue color area and the console will print "Blue Tapped".
Tap the "RED BUTTON" to switch to the other view.
Tap the red color area and the console will print "Red Tapped".
Now try to tap a blank space below the red area (where the blue area was previously located). The console will print "BLUE tapped" - this is the problem, it seems that the blue view is still active there.
I tested this behavior on: XCode 14.1, iPhone 13 Pro 16.1 iOS Simulator, and on a real iPhone with iOS 16. The result was always the same.
import SwiftUI
struct ContentView: View {
var body: some View {
NavView()
}
}
struct NavView: View {
#State private var colourShowed: Int = 1
var body: some View {
// If the DetailView() was shown directly, (without the NavigationLink and NavigationStack) there would be no such a bug.
// DetailView(colourShowed: $colourShowed)
// The bug is obvious when using the NavigationStack() with the NavigationLink()
NavigationStack {
Form {
NavigationLink(destination: { DetailView(colourShowed: $colourShowed) },
label: { Text("Detail View") })
}
}
}
}
struct DetailView: View {
// It seems like the problem is related to this #Binding property when used in conjunction
// with the NavigationLink in "NavView" View above.
#Binding var colourShowed: Int
var body: some View {
ScrollView {
VStack(spacing: 20){
HStack {
Button("BLUE BUTTON", action: {colourShowed = 1})
Spacer()
Button("RED BUTTON", action: {colourShowed = 2})
}
if colourShowed == 1 {
Color.blue
.frame(height: 500)
// the onTapeGesture() is stillActive here even when the "colourShowed" property is set to '2' so this
// view should therefore be deinitialized.
.onTapGesture {
print("BLUE tapped")
}
// The onAppear() doesn't execute when switching from the Red view to the Blue view.
// It seems like the "Blue" View does not deinitialize itself after being previously shown.
.onAppear(perform: {print("Blue appeared")})
}
else {
Color.red
.frame(height: 100)
.onTapGesture {
print("RED tapped")
}
.onAppear(perform: {print("Red appeared")})
}
}
}
}
}
Is there any solution to prevent this?
This is a common problem encountered by those new to Swift and value semantics, you can fix it by using something called a "capture list" like this:
NavigationLink(destination: { [colourShowed] in
It occurred because DetailView wasn't re-init with the new value of colourShowed when it changed. Nothing in body was using it so SwiftUI's dependency tracking didn't think body had to be recomputed. But since you rely on DetailView being init with a new value you have to add it to the capture list to force body to be recomputed and init a new DetailView.
Here are other questions about the same problem with .sheet and .task.

Force one NavigationLink to the detail view in SwiftUI

I have a master-(primary)-detail setup in my app, where most of the list items in the top-level NavigationView go to another list of submenu items, with .isDetailLink(false), and then those submenu items then navigate to the detail view. This is all working as intended.
I'm trying to also put in a NavigationLink in the top level list to got to my settings page, which I want to force into the detail view. There isn't a submenu for this link, so I tried to navigate to it directly and force .isDetailView(true). However, this causes the view to open in what would be the primary window above, but hidden at first. The detail window only has a back button, which then makes the intended view pop out from the menu column.
Is there a way to force only the settings menu item to open in the detail view, essentially skipping one navigation level?
Thanks!
Here's the simplified version of what I'm currently trying:
struct ContentView: View {
var body: some View {
NavigationView {
List {
NavigationLink(destination: SubMenu1()) {Text("MenuItem1")}
.isDetailLink(false)
NavigationLink(destination: SubMenu2()) {Text("MenuItem2")}
.isDetailLink(false)
NavigationLink(destination: SubMenu3()) {Text("MenuItem3")})
.isDetailLink(false)
NavigationLink(destination: SettingsView()) {Text("Settings")}
.isDetailLink(true)
}
}
}
}
struct SubMenu: View {
var body: some View {
List {
ForEach(menuItems, id: \.self) { item in
NavigationLink(destination: DetailView(item)) {
Text(item)
}
}
}
}
}
UPDATE
I've taken some screenshots to illustrate the issue:
The top level navigation view:
With one of the sub-menus selected, and .isDetailView(false). This is working properly:
Settings does not have a sub-menu like the others, so I want it to open directly on the right hand side. Instead, it opens like this, and is only revealed when the back button is pressed:
Hey, I have seen your screenshots. I Think you are using NavigationView 2 times there in Settings. Please remove any one of them, It should work fine!

Create a SwiftUI Sidebar

I want to build a very simple iOS 14 sidebar using SwiftUI.
The setup is quite simple, I have three views HomeView, LibraryView and SettingsView and an enum representing each screen.
enum Screen: Hashable {
case home, library, settings
}
My end-goal is to automatically switch between a tab view and a sidebar depending on the size class but some things don't quite work as expected.
The global state is owned by the MainNavigationView, which is also the root view for my WindowGroup.
struct MainNavigationView: View {
#State var screen: Screen? = .home
var body: some View {
NavigationView {
SidebarView(state: $screen)
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
The SidebarView is a simple List containing three NavigationLink, one for each Screen.
struct SidebarView: View {
#Binding var state: Screen?
var body: some View {
List {
NavigationLink(
destination: HomeView(),
tag: Screen.home,
selection: $state,
label: {
Label("Home", systemImage: "house" )
})
NavigationLink(
destination: LibraryView(),
tag: Screen.library,
selection: $state,
label: {
Label("Library", systemImage: "book")
})
NavigationLink(
destination: SettingsView(),
tag: Screen.settings,
selection: $state,
label: {
Label("Settings", systemImage: "gearshape")
})
}
.listStyle(SidebarListStyle())
.navigationTitle("Sidebar")
}
}
I use the NavigationLink(destination:tag:selection:label) initializer so that the selected screen is set in my MainNavigationView so I can reuse that for my TabView later.
However, a lot of things don't quite work as expected.
First, when launching the app in a portrait-mode iPad (I used the iPad Pro 11-inch simulator), no screen is selected when launching the app. Only after I click Back in the navigation bar, the initial screen shows and my home view gets shown.
The second weird thing is that the binding seems to be set to nil whenever the sidebar gets hidden. In landscape mode the view works as expected, however when toggling the sidebar to hide and then shown again, the selection gets lost.
The content view stays correct, but the sidebar selection is lost.
Are these just SwiftUI bugs or is there a different way to create a sidebar with a Binding?
You need to include a default secondary view within the NavigationView { }, usually it would be a placeholder but you could use the HomeScreen, e.g.
struct MainNavigationView: View {
#State var screen: Screen? = .home
var body: some View {
NavigationView {
SidebarView(state: $screen)
HomeScreen()
}
.navigationViewStyle(DoubleColumnNavigationViewStyle())
}
}
Regarding the cell not re-selecting - as of iOS 14.2 there is no list selection binding (when not in editing mode) so selection is lost. Although the List API has a $selection param, it is only supported on macOS at the moment. You can see that info the header:
/// On iOS and tvOS, you must explicitly put the list into edit mode for
/// the selection to apply.
It's a bit convoluted but it means that selection binding that we need for a sidebar is only for macOS, on iOS it is only for multi-select (i.e. checkmarks) in edit mode. The reason could be since UITableView's selection is event driven, maybe it wasn't possible to translate into SwiftUI's state driven nature. If you've ever tried to do state restoration with a view already pushed on a nav controller and try to show the cell unhighlight animation when popping back and that table view wasn't loaded and cell was never highlighted in the first place you'll know what I mean. It was a nightmare to load the table synchronously, make the selected cell be drawn and then start the unhighlight animation. I expect that Apple will be reimplementing List, Sidebar and NavigationView in pure SwiftUI to overcome these issues so for now we just have to live with it.
Once this has been fixed it will be as simple as List(selection:$screen) { } like how it would work on macOS. As a workaround on iOS you could highlight the icon or text in your own way instead, e.g. try using bold text:
NavigationLink(
destination: HomeView(),
tag: Screen.home,
selection: $state,
label: {
Label("Home", systemImage: "house" )
})
.font(Font.headline.weight(state == Screen.home ? .bold : .regular))
It doesn't look very nice when in compact because after popping the main view, the bold is removed when the row is un-highlighted. There might be a way to disable using bold in that case.
There are 2 other bugs you should be aware of:
In portrait the sidebar only shows on the second tap of the Sidebar nav button.
In portrait if you show the sidebar and select the same item that is already showing, the sidebar does not dismiss.

SwiftUI - Buttons not working with BorderlessButtonStyle

I am attempting to create a list row that contains among other things, multiple individually usable buttons. Previously, I was able to accomplish this by overlaying the buttons on top of the HStacks in the list, but recently this has ceased to function properly.
From searching around, I found this question where the accepted answer suggests using the BorderlessButtonStyle on all buttons to allow them to be individually clicked. However, whenever I apply this property to my button, as in the following code, the button no longer is able to be clicked at all.
Is this an issue with how I am implementing the BorderlessButtonStyle or a bug in SwiftUI?
List{
HStack {
Text("Display Name").bold()
Divider()
if !self.editing_name{
Text(Auth.auth().currentUser?.displayName ?? "Unknown Name")
}
else{
TextField("Name", text: self.$new_name)
}
Spacer()
Button(action: {
print("Edit")
}) {
Image(systemName: "pencil")
.font(.body)
.foregroundColor(Color.blue)
}.buttonStyle(BorderlessButtonStyle())
}
}
I am currently using Xcode 11.5 / iOS 13.5

How to disable SwiftUI's default behavior?

SwiftUI makes it very easy to build declarative UIs. However, sometimes they assume defaults that are not necessarily what we want.
Example:
When adding two buttons inside a list row, SwiftUI automatically makes the whole row touchable, and both button's actions are called on row tap. This is the default behavior, as demonstrated in their WWDC videos.
But I do not want this behavior. I want both buttons to work properly, and the row to not be tappable.
Question:
How can we tell our Guacamole expert (to use the WWDC reference) to stop assuming how I want my list (or any other behavior) to work?
Any help would be appreciated.
If the List's default behavior is not required, you could use a VStack:
struct ContentView: View {
var body: some View {
VStack {
Button(action: {
print("foo")
}) {
Image(systemName: "photo")
}
Button(action: {
print("bar")
}) {
Image(systemName: "photo")
}
}
}
}
However if List is really required, then it could be customized by writing a custom ListStyle.
(Also take a look at this question: How to change ListStyle in List.)
It seems that SomethingStyle protocol is the way Apple wants developers to use to modify native SwiftUI elements/behavior. Another example would be ButtonStyle or TextFieldStyle, etc.
I am under the impression that Apple wants to enforce their style guidelines. Also to add onto #backslash-f answer you could also just use a for each instead of a list, this will give you a similar effect and allow much more customization.
struct doubleList: View {
var body: some View {
VStack{
ForEach(1 ..< 10) {index in
Button(action: {
print("foo")
}) {
Image(systemName: "photo")
}
}
}
}
}
Another option to try would be to wrap an UITableView into an UIViewRepresentable and try to enable buttons that way
It seems there might be another way around this by using tap gestures
Image(systemName: "photo")
.gesture(TapGesture().onEnded() {
print("action2")
})

Resources