How do I show different buttons in the toolbar based on which field is focused in SwiftUI? - ios

I want to show different buttons based on what field is focused, but the focus state is allways one behind.
Here is the code:
struct ContentView: View {
enum FocusField {
case one
case two
}
#FocusState var focused: FocusField?
var body: some View {
VStack {
TextField("One", text: .constant(""))
.focused($focused, equals: .one)
TextField("Two", text: .constant(""))
.focused($focused, equals: .two)
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
if focused == .one {
Text("One")
} else if focused == .two {
Text("Two")
} else {
Text("None")
}
Spacer()
Button("Done") { focused = nil }
}
}
}
}

#FocusState can be a bit buggy at times. I could only speculate on the reason why your approach is not working. But consider this solution (tested on Xcode 13 and IOS 15):
struct ContentView33: View {
// add confomance to String
enum FocusField: String {
case one = "One" // add rawValues
case two = "Two"
}
#FocusState var focused: FocusField?
var body: some View {
VStack {
TextField("One", text: .constant(""))
.focused($focused, equals: .one)
TextField("Two", text: .constant(""))
.focused($focused, equals: .two)
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
//this will update the keyboard toolbar
Text(focused?.rawValue ?? "None")
Spacer()
Button("Done") { focused = nil }
}
}
}
}

Related

SwiftUI iOS15 Toolbar's content not refreshing based on the FocusState value

I have a view with two TextFields. When the first one is focused, I'd like to display Next button in the toolbar and when the second text field is focused, I'd like to present Previous and Done buttons in the toolbar.
I have an if statement inside the toolbar, but it looks like it doesn't pick up the change of #FocusState until I type something.
Any ideas how to make it work properly or why doesn't the toolbar pick up the changes?
This is more or less the code (I simplified the actual code):
import SwiftUI
import Combine
enum Field {
case inLangName
case outLangName
}
struct MyView: View {
#FocusState private var focusedTextField: Field?
#State var inLangName: String = ""
#State var outLangName: String = ""
var body: some View {
NavigationView {
VStack(spacing: 15) {
TextField("In lang name", text: $inLangName)
.focused($focusedTextField, equals: .inLangName)
.simultaneousGesture(TapGesture().onEnded { _ in
focusedTextField = .inLangName
})
TextField("Out lang name", text: $outLangName)
.focused($focusedTextField, equals: .outLangName)
.simultaneousGesture(TapGesture().onEnded { _ in
focusedTextField = .outLangName
})
}
.navigationBarHidden(true)
.navigationBarTitleDisplayMode(.inline)
.onAppear {
if inLangName.isEmpty {
focusedTextField = .inLangName
}
}
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
if focusedTextField == .inLangName || focusedTextField == nil {
Spacer()
Button(action: {
focusedTextField = .outLangName
}) {
Text("next")
}
} else if focusedTextField == .outLangName {
Button(action: {
focusedTextField = .inLangName
}) {
Text("previous")
}
Spacer()
Button(action: {
//onDoneButtonClicked()
}) {
Text("done")
}
}
}
}
}
}
}

SwiftUI Picker and Buttons inside same Form section are triggered by the same user click

I have this AddWorkoutView and I am trying to build some forms similar to what Apple did with "Add new contact" sheet form.
Right now I am trying to add a form more complex than a simple TextField (something similar to "add address" from Apple contacts but I am facing the following issues:
in the Exercises section when pressing on a new created entry (exercise), both Picker and delete Button are triggered at the same time and the Picker gets automatically closed as soon as it gets open and the selected entry is also deleted when going back to AddWorkoutView.
Does anyone have any idea on how Apple implemented this kind of complex form like in the screenshow below?
Thanks to RogerTheShrubber response here I managed to somehow implement at least the add button and to dynamically display all the content I previously added, but I don't know to bring together multiple TextFields/Pickers/any other stuff in the same form.
struct AddWorkoutView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#EnvironmentObject var dateModel: DateModel
#Environment(\.presentationMode) var presentationMode
#State var workout: Workout = Workout()
#State var exercises: [Exercise] = [Exercise]()
func getBinding(forIndex index: Int) -> Binding<Exercise> {
return Binding<Exercise>(get: { workout.exercises[index] },
set: { workout.exercises[index] = $0 })
}
var body: some View {
NavigationView {
Form {
Section("Workout") {
TextField("Title", text: $workout.title)
TextField("Description", text: $workout.description)
}
Section("Exercises") {
ForEach(0..<workout.exercises.count, id: \.self) { index in
HStack {
Button(action: { workout.exercises.remove(at: index) }) {
Image(systemName: "minus.circle.fill")
.foregroundColor(.red)
.padding(.horizontal)
}
Divider()
VStack {
TextField("Title", text: $workout.exercises[index].title)
Divider()
Picker(selection: getBinding(forIndex: index).type, label: Text("Type")) {
ForEach(ExerciseType.allCases, id: \.self) { value in
Text(value.rawValue)
.tag(value)
}
}
}
}
}
Button {
workout.exercises.append(Exercise())
} label: {
HStack(spacing: 0) {
Image(systemName: "plus.circle.fill")
.foregroundColor(.green)
.padding(.trailing)
Text("add exercise")
}
}
}
}
.navigationTitle("Create new Workout")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("Cancel")
}
.accessibilityLabel("Cancel adding Workout")
}
ToolbarItem(placement: .confirmationAction) {
Button {
} label: {
Text("Done")
}
.accessibilityLabel("Confirm adding the new Workout")
}
}
}
}
}

ToolbarItemGroup in .toolbar {} doesn't work in a Sheet

I'm using SwiftUI 3.0, Swift 5.5 and Xcode 13.2, tested on iOS 15.3 iPhone device, and iOS 15.2 iPhone simulator.
I have tested the following.
This is a view, with a TextField, a focused state and a .toolbar
import SwiftUI
struct test: View {
#State private var name = "Taylor Swift"
#FocusState var isInputActive: Bool
var body: some View {
TextField("Enter your name", text: $name)
.textFieldStyle(.roundedBorder)
.focused($isInputActive)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button(name) {
isInputActive = false
}
}
}
}
}
struct test_Previews: PreviewProvider {
static var previews: some View {
test()
}
}
It works perfectly as expected and it shows a button, with whatever text is typed in the TextField.
Then, when it's displayed in a sheet, there is no toolbar, though it is the same code. This is the sheet example:
import SwiftUI
struct test: View {
#State private var name = "Taylor Swift"
#FocusState var isInputActive: Bool
#State var isSheetPresented: Bool = false
var body: some View {
VStack {
Button {
self.isSheetPresented = true
} label: {
Text("Open Sheet")
}
}
.sheet(isPresented: $isSheetPresented) {
TextField("Enter your name", text: $name)
.textFieldStyle(.roundedBorder)
.focused($isInputActive)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button(name) {
isInputActive = false
}
}
}
}
}
}
struct test_Previews: PreviewProvider {
static var previews: some View {
test()
}
}
Toolbar needs a
NavigationView
And one at the top level. Surrounding the text field.
Today I also experienced the same thing. I had to spent many hours until finally got the solution after reading this question. I wanted to put "Done" button over my keyboard for dismissing it after editing is finish and I used ToolbarItem(placement: .keyboard).
In my case, I mistakenly put more than one .toolbar() in different places. And it causing the "Done" button in my sheet becoming disabled, something like this (in simulator):
In order to solve the problem, please DO NOT do this:
struct SettingsView: View {
var body: some View {
NavigationView {
Form {
// Some other codes..
}.navigationBarTitle("Settings", displayMode: .large).toolbar() { // <--- This one is a .toolbar()
ToolbarItem{
Button("Cancel"){
self.mode.wrappedValue.dismiss()
}
}}
}.toolbar { // <--- This one another .toolbar() (-_-")
ToolbarItem(placement: .keyboard) { // <--- This one is in the WRONG place!
Button("Done") {
focusedField = nil
}
}
}
}
}
Instead, do the following:
struct SettingsView: View {
var body: some View {
NavigationView {
Form {
// Some other codes..
}.navigationBarTitle("Settings", displayMode: .large).toolbar() { // Make it into a single .toolbar() 👍🏼
ToolbarItem{
Button("Cancel"){
self.mode.wrappedValue.dismiss()
}
}
ToolbarItem(placement: .keyboard) {
Button("Done") {
focusedField = nil
}
}
}
}
}
}
Hope it helps.

SwiftUI List selection doesn’t show If I add a NavigationLink and a .contextMenu to the list. Is this a known bug?

List selection doesn’t show If I add a NavigationLink and a .contextMenu to the list, when I select a row, the selection disappears.
struct ContentView: View {
#State private var selection: String?
let names = ["Cyril", "Lana", "Mallory", "Sterling"]
var body: some View {
NavigationView {
List(names, id: \.self, selection: $selection) { name in
NavigationLink(destination: Text("Hello, world!")) {
Text(name)
.contextMenu {
Button(action: {}) {
Text("Tap me!")
}
}
}
}
.toolbar {
EditButton()
}
}
}
}
We can disable context menu button(s) for the moment of construction in edit mode (because the button is a reason of issue).
Here is a possible approach - some redesign is required to handle editMode inside context menu (see also comments inline).
Tested with Xcode 13.2 / iOS 15.2
struct ContentViewSelection: View {
#State private var selection: String?
let names = ["Cyril", "Lana", "Mallory", "Sterling"]
var body: some View {
NavigationView {
List(names, id: \.self, selection: $selection) { name in
// separated view is needed to use editMode
// environment value
NameCell(name: name)
}
.toolbar {
EditButton()
}
}
}
}
struct NameCell: View {
#Environment(\.editMode) var editMode // << !!
let name: String
var body: some View {
NavigationLink(destination: Text("Hello, world!")) {
Text(name)
}
.contextMenu {
if editMode?.wrappedValue == .inactive { // << !!
Button(action: {}) {
Text("Tap me!")
}
}
}
}
}

Swiftui how to add a view above a tabview?

The requirement is to display a banner above the tabbar. So that the banner does not disappear when the tabs are changed? How can I achieve it?
I can think of starting points, to help you see ways to approach this, but I don't think either idea really meets your requirements. It will depend on the details of whether the banner is permanent, where its content comes from, etc.
The first idea:
struct BannerView : View {
var text : String
var body: some View {
VStack {
Spacer()
HStack {
Spacer()
Text(text)
Spacer()
}.background(Color.orange)
}
}
}
Then you can include this in a ZStack along with your tabs:
TabView {
ZStack {
Text("Tab 1")
BannerView("BANNER")
}.tabItem { Text("Home") }
ZStack {
Text("Tab 2")
BannerView("BANNER")
}.tabItem { Text("History") }
}
The second idea uses the BannerView from the first idea, but in a slightly cleaner way, still not great:
struct TabWrapperWithOptionalBanner<Content> : View where Content : View {
var showBanner : Bool
var content : Content
init(showBanner : Bool, #ViewBuilder content: () -> Content) {
self.showBanner = showBanner
self.content = content()
}
var body: some View {
ZStack {
content
if showBanner {
BannerView(text: "BANNER")
}
}
}
}
then your ContentView looks like this:
TabView {
TabWrapperWithOptionalBanner(showBanner: showBanner) {
Text("Tab 1")
}.tabItem { Text("Home") }
TabWrapperWithOptionalBanner(showBanner: showBanner) {
Text("Tab 2")
}.tabItem { Text("History") }
}
Update Try a pair of TabViews bound to the same State:
struct BanneredTabView: View {
#State private var selected = panels.one
var body: some View {
VStack {
TabView(selection: $selected) {
panels.one.label.tag(panels.one)
panels.two.label.tag(panels.two)
panels.three.label.tag(panels.three)
}
.tabViewStyle(PageTabViewStyle())
Text("Banner")
.frame(height: 40, alignment: .top)
TabView(selection: $selected) {
ForEach(panels.allCases) { panel in
Text("").tabItem {
panel.label
}
.tag(panel)
}
}
.frame(height: 30)
}
}
enum panels : Int, CaseIterable, Identifiable {
case one = 1
case two = 2
case three = 3
var label : some View {
switch self {
case .one:
return Label("Tab One", systemImage: "1.circle")
case .two:
return Label("Tab Two", systemImage: "2.square")
case .three:
return Label("Tab Three", systemImage: "asterisk.circle")
}
}
// so the enum can be identified when enumerated
var id : Int { self.rawValue }
}
}

Resources