Switch keyboard type in SwiftUI - swiftui

I have a textfield to enter search terms, plus a segmented control to indicate what database table should be searched:
struct ContentView: View {
enum SearchMode: Int {
case searchInNumbers, searchInAlphabet
}
#State private var mode = SearchMode.searchInNumbers
#State private var searchString = ""
private var keyboardType: UIKeyboardType {
if self.mode == .searchInNumbers {
NSLog("Setting keyboard to numbersAndPunctuation")
return .numbersAndPunctuation
} else {
NSLog("Setting keyboard to default")
return .default
}
}
var body: some View {
VStack {
TextField("Search string", text: self.$searchString)
.keyboardType(self.keyboardType)
.textFieldStyle(RoundedBorderTextFieldStyle())
Picker("Search in", selection: self.makePickerBinding()) {
Text("numbers").tag(0)
Text("alphabet").tag(1)
}.pickerStyle(SegmentedPickerStyle())
}.padding(20)
}
private func makePickerBinding() -> Binding<Int> {
Binding<Int>(
get: {
return self.mode.rawValue
},
set: { tag in
self.mode = SearchMode.init(rawValue: tag)!
}
)
}
}
The keyboard style should be the right one for the database table we search. This works, except when the keyboard is already up.
Meaning, when the user first taps the textfield, and then changes the segmented control, the keyboard type does not change.
I don't want to fix it with yet another wrapped UITextField. Can it be fixed in SwiftUI?

Related

Keyboard dips when switching text fields using onSubmit

I am capture the return key button pressed even in onSubmit. Switching state in this way, dismisses the keyboard, but presents it immediately again. So they keyboard doesn't fully dismiss, it just dips.
How do I prevent the keyboard dipping? I want it to remain on screen as I switch the focused field to another.
enum Field {
case first, second
}
struct ContentView: View {
#State private var content1 = ""
#State private var content2 = ""
#FocusState private var state: Field?
var body: some View {
VStack {
TextField("Hi1", text: $content1)
.focused($state, equals: .first)
TextField("Hi2", text: $content2)
.focused($state, equals: .second)
}
.onSubmit {
switch state {
case .none:
break
case .some(let wrapped):
switch wrapped {
case .first:
state = .second
case .second:
state = nil
}
}
}
.padding()
}
}

SwiftUI 4: NavigationSplitView behaves strangely?

In SwiftUI 4, there is now a NavigationSplitView. I played around with it and detected some strange behaviour.
Consider the following code: When the content function returns the plain Text, then there is the expected behaviour - tapping a menu item changes the detail view to the related text.
However, when commenting out the first four cases, and commenting in the next four, then a tap on "Edit Profile" does not change the detail view display. (Using #ViewBuilder does not change this behaviour.)
Any ideas out there about the reasons for that? From my point of view, this may just be a simple bug, but perhaps there are things to be considered that are not documented yet?!
struct MainScreen: View {
#State private var menuItems = MenuItem.menuItems
#State private var menuItemSelection: MenuItem?
var body: some View {
NavigationSplitView {
List(menuItems, selection: $menuItemSelection) { course in
Text(course.name).tag(course)
}
.navigationTitle("HappyFreelancer")
} detail: {
content(menuItemSelection)
}
.navigationSplitViewStyle(.balanced)
}
func content(_ selection: MenuItem?) -> some View {
switch selection {
case .editProfile:
return Text("Edit Profile")
case .evaluateProfile:
return Text("Evaluate Profile")
case .setupApplication:
return Text("Setup Application")
case .none:
return Text("none")
// case .editProfile:
// return AnyView(EditProfileScreen())
//
// case .evaluateProfile:
// return AnyView(Text("Evaluate Profile"))
//
// case .setupApplication:
// return AnyView(Text("Setup Application"))
//
// case .none:
// return AnyView(Text("none"))
}
}
}
struct MainScreen_Previews: PreviewProvider {
static var previews: some View {
MainScreen()
}
}
enum MenuItem: Int, Identifiable, Hashable, CaseIterable {
var id: Int { rawValue }
case editProfile
case evaluateProfile
case setupApplication
var name: String {
switch self {
case .editProfile: return "Edit Profile"
case .evaluateProfile: return "Evaluate Profile"
case .setupApplication: return "Setup Application"
}
}
}
extension MenuItem {
static var menuItems: [MenuItem] {
MenuItem.allCases
}
}
struct EditProfileScreen: View {
var body: some View {
Text("Edit Profile")
}
}
After playing around a bit in order to force SwiftUI to redraw the details view, I succeeded in this workaround:
Wrap the NavigationSplitView into a GeometryReader.
Apply an .id(id) modifier to the GeometryReader (e.g., as #State private var id: Int = 0)
In this case, any menu item selection leads to a redraw as expected.
However, Apple should fix the bug, which it is obviously.
I've found that wrapping the Sidebar list within its own view will fix this issue:
struct MainView: View {
#State var selection: SidebarItem? = .none
var body: some View {
NavigationSplitView {
Sidebar(selection: $selection)
} content: {
content(for: selection)
} detail: {
Text("Detail")
}
}
#ViewBuilder
func content(for item: SidebarItem?) -> some View {
switch item {
case .none:
Text("Select an Item in the Sidebar")
case .a:
Text("A")
case .b:
Text("B")
}
}
}

SwiftUI State var array not updating child views

For some reason I don't understand, when I add/remove items from a #State var in MainView, the OutterViews are not being updated properly.
What I am trying to achieve is that the user can only "flag" (select) one item at a time. For instance, when I click on "item #1" it will be flagged. If I click on another item then "item #1" will not be flagged anymore but only the new item I just clicked.
Currently, my code shows all items as if they were flagged even when they are not anymore. The following code has the minimum structure and functionality I'm implementing for MainView, OutterView, and InnerView.
I've tried using State vars instead of the computed property in OutterView, but it doesn't work. Also, I tried using a var instead of the computed property in OutterViewand initialized it in init() but also doesn't work.
Hope you can help me to find what I am doing wrong.
Thanks!
struct MainView: View {
#State var flagged: [String] = []
var data: [String] = ["item #1", "item #2", "item #3", "item #4", "item #5"]
var body: some View {
VStack(spacing: 50) {
VStack {
ForEach(data, id:\.self) { text in
OutterView(text: text, flag: flagged.contains(text)) { (flag: Bool) in
if flag {
flagged = [text]
} else {
if let index = flagged.firstIndex(of: text) {
flagged.remove(at: index)
}
}
}
}
}
Text("Flagged: \(flagged.description)")
Button(action: {
flagged = []
}, label: {
Text("Reset flagged")
})
}
}
}
struct OutterView: View {
#State private var flag: Bool
private let text: String
private var color: Color { flag ? Color.green : Color.gray }
private var update: (Bool)->Void
var body: some View {
InnerView(color: color, text: text)
.onTapGesture {
flag.toggle()
update(flag)
}
}
init(text: String, flag: Bool = false, update: #escaping (Bool)->Void) {
self.text = text
self.update = update
_flag = State(initialValue: flag)
}
}
struct InnerView: View {
let color: Color
let text: String
var body: some View {
Text(text)
.padding()
.background(
Capsule()
.fill(color))
}
}
Here's a simple version that does what you're looking for (explained below):
struct Item : Identifiable {
var id = UUID()
var flagged = false
var title : String
}
class StateManager : ObservableObject {
#Published var items = [Item(title: "Item #1"),Item(title: "Item #2"),Item(title: "Item #3"),Item(title: "Item #4"),Item(title: "Item #5")]
func singularBinding(forIndex index: Int) -> Binding<Bool> {
Binding<Bool> { () -> Bool in
self.items[index].flagged
} set: { (newValue) in
self.items = self.items.enumerated().map { itemIndex, item in
var itemCopy = item
if index == itemIndex {
itemCopy.flagged = newValue
} else {
//not the same index
if newValue {
itemCopy.flagged = false
}
}
return itemCopy
}
}
}
func reset() {
items = items.map { item in
var itemCopy = item
itemCopy.flagged = false
return itemCopy
}
}
}
struct MainView: View {
#ObservedObject var stateManager = StateManager()
var body: some View {
VStack(spacing: 50) {
VStack {
ForEach(Array(stateManager.items.enumerated()), id:\.1.id) { (index,item) in
OutterView(text: item.title, flag: stateManager.singularBinding(forIndex: index))
}
}
Text("Flagged: \(stateManager.items.filter({ $0.flagged }).map({$0.title}).description)")
Button(action: {
stateManager.reset()
}, label: {
Text("Reset flagged")
})
}
}
}
struct OutterView: View {
var text: String
#Binding var flag: Bool
private var color: Color { flag ? Color.green : Color.gray }
var body: some View {
InnerView(color: color, text: text)
.onTapGesture {
flag.toggle()
}
}
}
struct InnerView: View {
let color: Color
let text: String
var body: some View {
Text(text)
.padding()
.background(
Capsule()
.fill(color))
}
}
What's happening:
There's a Item that has an ID for each item, the flagged state of that item, and the title
StateManager keeps an array of those items. It also has a custom binding for each index of the array. For the getter, it just returns the state of the model at that index. For the setter, it makes a new copy of the item array. Any time a checkbox is set, it unchecks all of the other boxes.
The ForEach now gets an enumeration of the items. This could be done without enumeration, but it was easy to write the custom binding by index like this. You could also filter by ID instead of index. Note that because of the enumeration, it's using .1.id for the id parameter -- .1 is the item while .0 is the index.
Inside the ForEach, the custom binding from before is created and passed to the subview
In the subview, instead of using #State, #Binding is used (this is what the custom Binding is passed to)
Using this strategy of an ObservableObject that contains all of your state and passes it on via #Published properties and #Bindings makes organizing your data a lot easier. It also avoids having to pass closures back and forth like you were doing initially with your update function. This ends up being a pretty idiomatic way of doing things in SwiftUI.

SwiftUI selection in lists not working on reused cells

Consider the following project with two views. The first view presents the second one:
import SwiftUI
struct ContentView: View {
private let data = 0...1000
#State private var selection: Set<Int> = []
#State private var shouldShowSheet = false
var body: some View {
self.showSheet()
//self.showPush()
}
private func showSheet() -> some View {
Button(action: {
self.shouldShowSheet = true
}, label: {
Text("Selected: \(selection.count) items")
}).sheet(isPresented: self.$shouldShowSheet) {
EditFormView(selection: self.$selection)
}
}
private func showPush() -> some View {
NavigationView {
Button(action: {
self.shouldShowSheet = true
}, label: {
NavigationLink(destination: EditFormView(selection: self.$selection),
isActive: self.$shouldShowSheet,
label: {
Text("Selected: \(selection.count) items")
})
})
}
}
}
struct EditFormView: View {
private let data = 0...1000
#Binding var selection: Set<Int>
#State private var editMode: EditMode = .active
init(selection: Binding<Set<Int>>) {
self._selection = selection
}
var body: some View {
List(selection: self.$selection) {
ForEach(data, id: \.self) { value in
Text("\(value)")
}
}.environment(\.editMode, self.$editMode)
}
}
Steps to reproduce:
Create an app with the above two views
Run the app and present the sheet with the editable list
Select some items at random indexes, for example a handful at index 0-10 and another handful at index 90-100
Close the sheet by swiping down/tapping back button
Open the sheet again
Scroll to indexes 90-100 to view the selection in the reused cells
Expected:
The selected indexes as you had will be in “selected state”
Actual:
The selection you had before is not marked as selected in the UI, even though the binding passed to List contains those indexes.
This occurs both on the “sheet” presentation and the “navigation link” presentation.
If you select an item in the list, the “redraw” causes the original items that were originally not shown as selected to now be shown as selected.
Is there a way around this?
It looks like EditMode bug, worth submitting feedback to Apple. The possible solution is to use custom selection feature.
Here is a demo of approach (modified only part). Tested & worked with Xcode 11.4 / iOS 13.4
struct EditFormView: View {
private let data = 0...1000
#Binding var selection: Set<Int>
init(selection: Binding<Set<Int>>) {
self._selection = selection
}
var body: some View {
List(selection: self.$selection) {
ForEach(data, id: \.self) { value in
self.cell(for: value)
}
}
}
// also below can be separated into standalone view
private func cell(for value: Int) -> some View {
let selected = self.selection.contains(value)
return HStack {
Image(systemName: selected ? "checkmark.circle" : "circle")
.foregroundColor(selected ? Color.blue : nil)
.font(.system(size: 24))
.onTapGesture {
if selected {
self.selection.remove(value)
} else {
self.selection.insert(value)
}
}.padding(.trailing, 8)
Text("\(value)")
}
}
}

Unable to repeat Picker Selection

Scenario:
I have a simple picker within a form.
I select a picker item (with chevron) from the form row.
I choose an item (row) from a list of items in the result panel.
The result panel slides away to reveal the original panel.
I am NOT able to repeat this procedure.
Here's my code:
class ChosenView: ObservableObject {
static let choices = ["Modal", "PopOver", "Circle", "CircleImage", "Scroll", "Segment", "Tab", "Multi-Line"]
#Published
var type = 0
}
struct ContentView: View {
#ObservedObject var chosenView = ChosenView()
#State private var isPresented = false
var body: some View {
VStack {
NavigationView {
Form {
Picker(selection: $chosenView.type, label: Text("The Panels")) {
ForEach(0..<ChosenView.choices.count) {
Text(ChosenView.choices[$0]).tag($0)
}
}
}.navigationBarTitle(Text("Available Views"))
.actionSheet(isPresented: $isPresented, content: {
ActionSheet(title: Text("Hello"))
})
}
Section {
Button(action: launchView) {
Text("Select: \(ChosenView.choices[chosenView.type])")
}
}
Spacer()
}
}
private func launchView() {
isPresented = true
}
}
What am I missing?
Why can't I repeat picker selection rather than having to reboot?