SwiftUI How to add done button to picker - xcode

Ive make a mini app with just a button and a picker and the idea is to have a done button above the picker so once ive chosen a value i can press done and the picker will close.
I am aware if you click the "click me" button it will open and if you click it again close the picker but im looking for a button that appears with the picker and disapears with the clicker when clicked.
Almost like a toolbar above the picker with a done button
#State var expand = false
#State var list = ["value1", "value2", "value3"]
#State var index = 0
var body: some View {
VStack {
Button(action: {
self.expand.toggle()
}) {
Text("Click me \(list[index])")
}
if expand {
Picker(selection: $list, label: EmptyView()) {
ForEach(0 ..< list.count) {
Text(self.list[$0]).tag($0)
}
}.labelsHidden()
}
}
The third image is what im trying to accomplish and the first 2 are what ive currently got
Thank you for your help

Add a Button to the if-clause:
if expand {
VStack{
Button(action:{self.expand = false}){
Text("Done")
}
Picker(selection: $list, label: EmptyView()) {
ForEach(0 ..< list.count) {
Text(self.list[$0]).tag($0)
}
}.labelsHidden()
}
}

Here is an approach how I would do this... of course tuning is still possible (animations, rects, etc.), but the direction of idea should be clear
Demo of result:
Code:
struct ContentView: View {
#State var expand = false
#State var list = ["value1", "value2", "value3"]
#State var index = 0
var body: some View {
VStack {
Button(action: {
self.expand.toggle()
}) {
Text("Click me \(list[index])")
}
if expand {
Picker(selection: $index, label: EmptyView()) {
ForEach(0 ..< list.count) {
Text(self.list[$0]).tag($0)
}
}.labelsHidden()
.overlay(
GeometryReader { gp in
VStack {
Button(action: {
self.expand.toggle()
}) {
Text("Done")
.font(.system(size: 42))
.foregroundColor(.red)
.padding(.vertical)
.frame(width: gp.size.width)
}.background(Color.white)
Spacer()
}
.frame(width: gp.size.width, height: gp.size.height - 12)
.border(Color.black, width: 8)
}
)
}
}
}
}

Also this would work (especially if you're doing it outside of a Vstack)...
// Toolbar for "Done"
func createToolbar() {
let toolBar = UIToolbar()
toolBar.sizeToFit()
// "Done" Button for Toolbar on Picker View
let doneButton = UIBarButtonItem(title: "Done", style: .plain, target:
self, action: #selector(PageOneViewController.dismissKeyboard))
toolBar.setItems([doneButton], animated: false)
toolBar.isUserInteractionEnabled = true
// Makes Toolbar Work for Text Fields
familyPlansText.inputAccessoryView = toolBar
kidsOptionText.inputAccessoryView = toolBar
ethnicityText.inputAccessoryView = toolBar
}
And be sure to call createToolbar() and you're done!

struct ContentView: View {
var colors = ["Red", "Green", "Blue"]
#State private var selectedColor = 0
var body: some View {
NavigationView {
Form {
Section {
Picker(selection: $selectedColor, label: Text("Color")) {
ForEach(0 ..< colors.count) {
Text(self.colors[$0])
}
}
}
}
}
}
}
This has some form specific picker behavior where it opens up inline with no button hiding needed.

Related

SwiftUI - MacOS - TextField within Alert not receiving focus

I've added a TextField within an Alert in MacOS. However, the TextField doesn't receive focus, when the alert shows. Here's the code I've tested with. Is this even possible? If this is a bug, then please suggest other workarounds.
import SwiftUI
struct ContentView: View {
#State private var presented = false
#State private var username = ""
#FocusState private var focused: Bool
var body: some View {
VStack {
Button(action: {
focused = true
presented = true
}, label: {
Text("Click to show Alert")
})
}
.alert("", isPresented: $presented, actions: {
VStack {
TextField("User name (email address)", text: $username)
.focusable()
.focused($focused)
Button(action: {}, label: { Text("OK") })
}
.onAppear() {
// Try setting focus with a delay.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: {
focused = true
})
}
}, message: {
Text("The textField in this alert doesn't get the focus. Therefore, a manual click is needed to set focus and start typing.")
})
}
}
I can confirm that focus doesn't work inside Alert. A possible workaround is using a custom Sheet:
struct ContentView: View {
#State private var presented = false
#State private var username = ""
#FocusState private var focused: Bool
var body: some View {
VStack {
Button(action: {
presented = true
focused = true
}, label: {
Text("Click to show Sheet")
})
}
.sheet(isPresented: $presented, content: {
VStack {
// App Icon placeholder
RoundedRectangle(cornerRadius: 8)
.fill(.secondary)
.frame(width: 48, height: 48)
.padding(12)
Text("The textField in this alert doesn't get the focus. Therefore, a manual click is needed to set focus and start typing.")
.font(.caption)
.multilineTextAlignment(.center)
TextField("User name (email address)", text: $username)
.focused($focused)
.padding(.vertical)
Button(action: {
presented = false
}, label: {
Text("OK")
.frame(maxWidth: .infinity)
})
.buttonStyle(.borderedProminent)
}
.padding()
.frame(width: 260)
})
}
}

SwiftUI: animating tab item addition/removal in tab bar

In my app I add/remove a subview to/from a TabView based on some condition. I'd like to animate tab item addition/removal in tab bar. My experiment (see code below) shows it's not working. I read on the net that TabView support for animation is quite limited and some people rolled their own implementation. But just in case, is it possible to implement it?
import SwiftUI
struct ContentView: View {
#State var showBoth: Bool = false
var body: some View {
TabView {
Button("Test") {
withAnimation {
showBoth.toggle()
}
}
.tabItem {
Label("1", systemImage: "1.circle")
}
if showBoth {
Text("2")
.tabItem {
Label("2", systemImage: "2.circle")
}
.transition(.slide)
}
}
}
}
Note: moving transition() call to the Label passed to tabItem() doesn't work either.
As commented Apple wants the TabBar to stay unchanged throughout the App.
But you can simply implement your own Tabbar with full control:
struct ContentView: View {
#State private var currentTab = "One"
#State var showBoth: Bool = false
var body: some View {
VStack {
TabView(selection: $currentTab) {
// Tab 1.
VStack {
Button("Toggle 2. Tab") {
withAnimation {
showBoth.toggle()
}
}
} .tag("One")
// Tab 2.
VStack {
Text("Two")
} .tag("Two")
}
// custom Tabbar buttons
Divider()
HStack {
OwnTabBarButton("One", imageName: "1.circle")
if showBoth {
OwnTabBarButton("Two", imageName: "2.circle")
.transition(.scale)
}
}
}
}
func OwnTabBarButton(_ label: String, imageName: String) -> some View {
Button {
currentTab = label
} label: {
VStack {
Image(systemName: imageName)
Text(label)
}
}
.padding([.horizontal,.top])
}
}

SwiftUI presenting sheet with Binding variable doesn't work when first presented

I'm trying to present a View in a sheet with a #Binding String variable that just shows/binds this variable in a TextField.
In my main ContentView I have an Array of Strings which I display with a ForEach looping over the indices of the Array, showing a Button each with the text of the looped-over-element.
The Buttons action is simple: set an #State "index"-variable to the pressed Buttons' Element-index and show the sheet.
Here is my ContentView:
struct ContentView: View {
#State var array = ["first", "second", "third"]
#State var showIndex = 0
#State var showSheet = false
var body: some View {
VStack {
ForEach (0 ..< array.count, id:\.self) { i in
Button("\(array[i])") {
showIndex = i
showSheet = true
}
}
// Text("\(showIndex)") // if I uncomment this line, it works!
}
.sheet(isPresented: $showSheet, content: {
SheetView(text: $array[showIndex])
})
.padding()
}
}
And here is the SheetView:
struct SheetView: View {
#Binding var text: String
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
TextField("text:", text: $text)
Button("dismiss") {
presentationMode.wrappedValue.dismiss()
}
}.padding()
}
}
The problem is, when I first open the app and press on the "second" Button, the sheet opens and displays "first" in the TextField. I can then dismiss the Sheet and press the "second" Button again with the same result.
If I then press the "third" or "first" Button everything works from then on. Pressing any Button results in the correct behaviour.
Preview
Interestingly, if I uncomment the line with the Text showing the showIndex-variable, it works from the first time on.
Is this a bug, or am I doing something wrong here?
You should use custom Binding, custom Struct for solving the issue, it is complex issue. See the Example:
struct ContentView: View {
#State private var array: [String] = ["first", "second", "third"]
#State private var customStruct: CustomStruct?
var body: some View {
VStack {
ForEach (array.indices, id:\.self) { index in
Button(action: { customStruct = CustomStruct(int: index) }, label: {
Text(array[index]).frame(width: 100)
})
}
}
.frame(width: 300, height: 300, alignment: .center)
.background(Color.gray.opacity(0.5))
.sheet(item: $customStruct, content: { item in SheetView(text: Binding.init(get: { () -> String in return array[item.int] },
set: { (newValue) in array[item.int] = newValue }) ) })
}
}
struct CustomStruct: Identifiable {
let id: UUID = UUID()
var int: Int
}
struct SheetView: View {
#Binding var text: String
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
TextField("text:", text: $text)
Button("dismiss") {
presentationMode.wrappedValue.dismiss()
}
}.padding()
}
}
I had this happen to me before. I believe it is a bug, in that until it is used in the UI, it doesn't seem to get set in the ForEach. I fixed it essentially in the same way you did, with a bit of subtlety. Use it in each Button as part of the Label but hide it like so:
Button(action: {
showIndex = i
showSheet = true
}, label: {
HStack {
Text("\(array[i])")
Text(showIndex.description)
.hidden()
}
})
This doesn't change your UI, but you use it so it gets properly updated. I can't seem to find where I had the issue in my app, and I have changed the UI to get away from this, but I can't remember how I did it. I will update this if I can find it. This is a bit of a kludge, but it works.
Passing a binding to the index fix the issue like this
struct ContentView: View {
#State var array = ["First", "Second", "Third"]
#State var showIndex: Int = 0
#State var showSheet = false
var body: some View {
VStack {
ForEach (0 ..< array.count, id:\.self) { i in
Button(action:{
showIndex = i
showSheet.toggle()
})
{
Text("\(array[i])")
}.sheet(isPresented: $showSheet){
SheetView(text: $array, index: $showIndex)
}
}
}
.padding()
}
}
struct SheetView: View {
#Binding var text: [String]
#Binding var index: Int
#Environment(\.presentationMode) var presentationMode
var body: some View {
VStack {
TextField("text:", text: $text[index])
Button("dismiss") {
presentationMode.wrappedValue.dismiss()
}
}.padding()
}
}
In SwiftUI2 when calling isPresented if you don't pass bindings you're going to have some weird issues.
This is a simple tweak if you want to keep it with the isPresented and make it work but i would advise you to use the item with a costum struct like the answer of swiftPunk
This is how I would do it. You'll lose your form edits if you don't use #State variables.
This Code is Untested
struct SheetView: View {
#Binding var text: String
#State var draft: String
#Environment(\.presentationMode) var presentationMode
init(text: Binding<String>) {
self._text = text
self._draft = State(initialValue: text.wrappedValue)
}
var body: some View {
VStack {
TextField("text:", text: $draft)
Button("dismiss") {
text = draft
presentationMode.wrappedValue.dismiss()
}
}.padding()
}
}

SwiftUI List how to identify what item is selected on macOS

Here is what I have based on this answer. The code currently allows the user to select a cell but I cannot distinguish which cell is selected or execute any code in response to the selection. In summary, how can I execute code based on the selected cell's name and execute on click. The cell currently highlights in blue where clicked, but I want to identify it and act accordingly based on that selection. Note: I am not looking to select the cell in editing mode. Also, how can I programmatically select a cell without click?
struct OtherView: View {
#State var list: [String]
#State var selectKeeper = Set<String>()
var body: some View {
NavigationView {
List(list, id: \.self, selection: $selectKeeper) { item in
Text(item)
}
}
}
}
Here is a gif demoing the selection
I found a workaround, but the text itself has to be clicked- clicking the cell does nothing:
struct OtherView: View {
#State var list: [String]
#State var selectKeeper = Set<String>()
var body: some View {
NavigationView {
List(list, id: \.self, selection: $selectKeeper) { item in
Text(item)
.onTapGesture {
print(item)
}
}
}
}
}
List selection works in Edit mode, so here is some demo of selection usage
struct OtherView: View {
#State var list: [String] = ["Phil Swanson", "Karen Gibbons", "Grant Kilman", "Wanda Green"]
#State var selectKeeper = Set<String>()
var body: some View {
NavigationView {
List(list, id: \.self, selection: $selectKeeper) { item in
if self.selectKeeper.contains(item) {
Text(item).bold()
} else {
Text(item)
}
}.navigationBarItems(trailing: HStack {
if self.selectKeeper.count != 0 {
Button("Send") {
print("Sending selected... \(self.selectKeeper)")
}
}
EditButton()
})
}
}
}
To spare you the labour pains:
import SwiftUI
struct ContentView: View {
#State private var selection: String?
let list: [String] = ["First", "Second", "Third"]
var body: some View {
NavigationView {
HStack {
List(selection: $selection) {
ForEach(list, id: \.self) { item in
VStack {
Text(item)
}
}
}
TextField("Option", text: Binding(self.$selection) ?? .constant(""))
}
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 100, maxHeight: .infinity)
}
}
}
This solution deals with the problem #Isaac addressed.
screenshot
my 2 cents for custom selection and actions
works in swift 5.1 / iOS14:
see:
https://gist.github.com/ingconti/49497419e5edd84a5f3be52397c76eb4
I left a lot of debug "print".. to better understand.
You can react to the selected item by using onChange on your selection variable.
You can set the selection manually by simply setting the variable. This will also trigger onChange.
Here's some example code that will run directly in Playgrounds:
import SwiftUI
import PlaygroundSupport
struct OtherView: View {
#State var list: [String] = ["a", "b", "c"]
#State var selection: String?
var body: some View {
NavigationView {
VStack {
List(list, id: \.self, selection: $selection) { item in
Text(item)
}
.onChange(of: selection) {s in
// React to changes here
print(s)
}
Button("Select b") {
// Example of setting selection programmatically
selection = "b"
}
}
}
}
}
let view = OtherView()
PlaygroundPage.current.setLiveView(view)

Has anyone updated SwiftUI Popovers to work in Xcode beta-3?

Updated to Xcode beta-3, Popover was deprecated... having one hell of a time trying to figure out how to make it work again!?!?
It no longer "pops up" it slides up from the bottom.
It's no longer positioned or sized correctly, takes up the whole screen.
Once dismissed, it never wants to appear again.
This was the old code, that worked perfectly...
struct ExerciseFilterBar : View {
#Binding var filter: Exercise.Filter
#State private var showPositions = false
var body: some View {
HStack {
Spacer()
Button(action: { self.showPositions = true } ) {
Text("Position")
}
.presentation(showPositions ? Popover(content: MultiPicker(items: Exercise.Position.allCases, selected:$filter.positions),
dismissHandler: { self.showPositions = false })
: nil)
}
.padding()
}
}
And this is the new code...
struct ExerciseFilterBar : View {
#Binding var filter: Exercise.Filter
#State private var showPositions = false
var body: some View {
HStack {
Spacer()
Button(action: { self.showPositions = true } ) {
Text("Position")
}
.popover(isPresented: $showPositions) {
MultiPicker(items: Exercise.Position.allCases, selected:self.$filter.positions)
.onDisappear { self.showPositions = false }
}
}
.padding()
}
}
I ended up using PresentationLink just so I can move forward with everything else...
struct ExerciseFilterBar : View {
#Binding var filter: Exercise.Filter
var body: some View {
HStack {
Spacer()
PresentationLink(destination: MultiPicker(items: Exercise.Position.allCases, selected:$filter.positions)) {
Text("Position")
}
}
.padding()
}
}
It works, as far as testing is concerned, but it's not a popover.
Thanks for any suggestions!
BTW, this code is being in the iPad simulator.
On OSX the code below works fine
struct ContentView : View {
#State var poSelAbove = false
#State var poSelBelow = false
#State var pick : Int = 1
var body: some View {
let picker = Picker(selection: $pick, label: Text("Pick option"), content:
{
Text("Option 0").tag(0)
Text("Option 1").tag(1)
Text("Option 2").tag(2)
})
let popoverWithButtons =
VStack {
Button("Not Dismiss") {
}
Divider()
Button("Dismiss") {
self.poSelAbove = false
}
}
.padding()
return VStack {
Group {
Button("Show button popover above") {
self.poSelAbove = true
}.popover(isPresented: $poSelAbove, arrowEdge: .bottom) {
popoverWithButtons
}
Divider()
Button("Show picker popover below") {
self.poSelBelow = true
}.popover(isPresented: $poSelBelow, arrowEdge: .top) {
Group {
picker
}
}
}
Divider()
picker
.frame(width: 300, alignment: .center)
Text("Picked option: \(self.pick)")
.font(.subheadline)
}
// comment the line below for iOS
.frame(width: 800, height: 600)
}
On iOS (iPad) the popover will appear in a strange transparent full screen mode. I don't think this is intended. I have added the problem to my existing bug report.

Resources