SwiftUi how to display a keyboard with numpad in the side - swiftui

I need to display a full keyboard in my app with a numpad in the side, like the one I screenshot from a Boeing App.
I try the .keyboardType(.numberPad) and many other Modifiers, but I could not get the same keyboard as the App from Boeing mentioned before.
Any thoughts?
Thanks
Keyboard I'm looking for

The basic Apple keyboard includes numbers on the second page. I would just use that if you need characters and numbers, or use the .numberPad if you need only numbers. Unless you're using this keyboard for fullscreen iOS or the iPad, your screenshot looks too wide too for the UI to work. Additionally, building a keyboard would be a lot of work for any benefits your custom keyboard might provide.
Here's a working example of a custom keyboard to get you started. Obviously you'll need to work on the appearance and adding some more buttons, but this will illustrate the basic structure of what you want.
struct CustomKeybaord: View {
#State var text: String = ""
let topCharacters: [String] = ["Q", "W", "E", "R", "T", "Y", "U", "I","O", "P"]
let topMiddleCharacters: [String] = ["A", "S", "D", "F", "G", "H", "J", "K", "L"]
let bottomMiddleCharacters: [String] = ["Z", "X", "C", "V", "B", "N", "M"]
func keyboardTap(character: String) {
self.text += character
}
var body: some View {
VStack {
Text(text)
HStack {
ForEach(topCharacters, id: \.self) { character in
KeyboardButton(character: character, color: .white, function: keyboardTap)
.padding(5)
}
}
HStack {
ForEach(topMiddleCharacters, id: \.self) { character in
KeyboardButton(character: character, color: .white, function: keyboardTap)
.padding(5)
}
}
.padding(.horizontal, 50)
HStack {
ForEach(bottomMiddleCharacters, id: \.self) { character in
KeyboardButton(character: character, color: .white, function: keyboardTap)
.padding(5)
}
}
.padding(.horizontal, 75)
}
.background(Color.init(white: 0.9))
.frame(height: 100)
}
}
struct KeyboardButton: View {
let character: String
let color: Color
var function: (String) -> Void
var body: some View {
Button {
function(character)
} label: {
Text(character)
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(color)
.foregroundColor(.black)
}
}
}
You'll also want to abstract away the HStacked ForEachs into an intermediate view.
The hardest thing left for you to do is find a way to calculate the key button sizes responsibly. Unless you want them to be a fixed size (which would be a bad idea, because it wouldn't work for any but one iPad size) you'll have figure out a way to calculate a universal button size based on the overall screen width using GeometryReader. In my example I use frame(maxWidth: .infinity, maxHeight: .infinity) to give each each button in a row the same size, but this doesn't ensure that buttons on different rows will be the same size.

Related

SWIFTUI: app buttons logic - change label in specific scenario

My app has a number of mainButton created by a single struct. The data is passed to these buttons from an array. Then I have a Submit button.
I am trying to have the Submit button to change label to a arrow up icon only when: a given instance of mainButton has been pressed once already and changed its color and size already (this works), and Submit was pressed once already. Now the Submit button's icon changes to a refresh symbol (arrow up), until a diffrent instance of mainButton has been pressed (and this new button in turn has changed the color and size already as expected).
Basically a button representing a value is selected(tapped) and then its content submitted by pressing the Submit button, and if you submit the same button again the submitter is a refresher (arrow up icon) rather than a submitter (Submit label) as you have submitted that given query before; hence if you want to submit the same value again, the button is submit button is labeled as a refresher. The code below should make this clearer.
I am new to Swift/SwiftUI and trying to do this in a simple way as I want to understand it fully. Hence, if possible, I would rather use if conditions and variables rather than advanced methods.
Here is a simplified version of my code with only place holders in it:
//
// AddView.swift
// WriteOn
//
// Created by Maurizio Zappettini on 2/11/23.
//
import SwiftUI
struct AddView: View {
#State private var typeofMessageGeneric: String = ""
#State private var lastButtonTapped: String = ""
#State private var buttonCount: Int = 0
let buttonData = [
(mainButtonText: "A", mainButtonValue: "a"),
(mainButtonText: "B", mainButtonValue: "b"),
(mainButtonText: "C", mainButtonValue: "c"),
(mainButtonText: "D", mainButtonValue: "d"),
]
var body: some View {
ZStack { // Using ZStack for background gradient
Rectangle()
.frame(
minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,
alignment: .topLeading)
.foregroundColor(.clear)
.background(LinearGradient(colors: [.blue, .purple], startPoint: .top, endPoint: .bottom))
VStack { //Master VStack
VStack { //Headline
Text("Test View")
.font(.headline)
.padding()
}
GeometryReader { geometry in // Debug alignment
VStack(alignment: .center) { //TextBox
Rectangle()
.fill(Color.cyan)
.cornerRadius(20)
.frame(width: geometry.size.width * 0.8, height: 250, alignment: .center)
.overlay(
HStack(alignment: .top){
VStack {
Spacer()
}
ScrollView {
VStack(alignment: .center) {
Text("getResponsePlaceHolder")
.foregroundColor(.black)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .center)
.layoutPriority(2)
}
}
VStack{
ShareLink(item: "nothing"){
Image(systemName: "square.and.arrow.up")
.resizable()
.scaledToFit()
.frame(width: 25, height: 25)
.foregroundColor(.black)
}
}
VStack{
Spacer()
}
}
.frame(height: geometry.size.height * 0.6) // find way to make it refer to actual box and not whole screen
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
} //Vstack Top
} //Geo Reader
Spacer(minLength: 50
)
ScrollView{
VStack { //Buttons
ForEach(buttonData, id: \.mainButtonValue) { data in
mainButton(typeofMessageGeneric: self.$typeofMessageGeneric, lastButtonTapped: self.$lastButtonTapped, buttonCount: self.$buttonCount, mainButtonText: data.mainButtonText, mainButtonValue: data.mainButtonValue)
}
} //Button Vstack end
}
VStack{ //Footer
submitButton()
}
}
}
}
func submitButton() -> some View {
return Button(action: getResponse) {
if self.typeofMessageGeneric == self.lastButtonTapped && self.buttonCount == 0 {
Text("Submit")
.font(.system(size: 20))
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(10)
} else {
Image(systemName: "arrow.clockwise.circle")
.font(.system(size: 30))
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(10)
}
}
}
// Create main buttons
struct mainButton: View {
#Binding var typeofMessageGeneric: String // Bind the variable inside this struct with the same one inside the ContentView struct
#Binding var lastButtonTapped: String // Not currently used -- implement later if needed
#Binding var buttonCount:Int
var mainButtonText:String
var mainButtonValue:String
var body: some View {
Button(action: {
self.typeofMessageGeneric = mainButtonValue
self.lastButtonTapped = mainButtonValue
buttonCount = 0
if self.typeofMessageGeneric == mainButtonValue{
buttonCount = 1
} else {
buttonCount = 0
}
}) {
Text(mainButtonText)
Text(String(buttonCount)) // testing only
}
.font(.system(size: 20))
.foregroundColor(.white)
.padding()
.background(self.mainButtonValue == self.typeofMessageGeneric ? Color.teal : Color.purple)
.cornerRadius(10)
.scaleEffect(self.mainButtonValue == self.typeofMessageGeneric ? 1.12 : 1)
.animation(.easeOut, value: 2)
}
}
func getResponse() {
}
}
struct AddView_Previews: PreviewProvider {
static var previews: some View {
AddView()
}
}
EDIT:
Here is a better explanation of what the app does and what I am trying to achieve in terms of button logic.
What's on the screen when you start the app:
There is an empty text box.
There are 4 buttons (it will be more)
There is a Submit button at the bottom
What the app does:
The 4 main buttons are 4 different prompts. Each prompt/button is composed of a label and an associated value.
Tapping a button acts as a way to select that button's corresponding value.
Tapping the submit button will submit the value of the last selected main buttons to an external API. The latter will return some specific data that is visualized in the text box above (this part I left out as it is unrelated to my button label problem).
If the same prompt is submitted again the API will return different data (as there is a randomization mechanism). Hence, the user may want to submit again the same prompt they have submitted already.
How the buttons behave (practical example):
1- You tap button B
2- Button B changes the background color from purple to teal and becomes bigger.
3- You tap the "Submit" button.
4- The submit button changes its label from "Submit" to ARROW_UP.
5- The app submits value "b" to the API and this returns DATA_b_1.
6- DATA_b_1 is visualized in the text box.
7- Button B is still 'selected' (it is still teal and larger)
8- You tap ARROW_UP.
9- Value "b" is submitted again to the API and this now returns
DATA_b_2 (a different random instance of DATA_b).
10- DATA_b_2 is visualized in the text box.
11- You now tap button D.
12- Button D changes the background color from purple to teal and becomes bigger.
13- Button B reverts its color to purple and its size to smaller.
14- ARROW_UP button reverts its label to "Submit".
15- Back to point 3
Everything in the list above works as expected except the "Submit" to ARROW_UP and vice-versa label changes.
I hope I understood what you are trying to achieve, the. question is not very clear to me. What I got is:
Show the question and 4 possible answers to the user. Let the user select an answer. Submit button: disabled.
The user selects one answer; they can change their mind as much as they want until the answer is submitted. Submit button: enabled.
The user pressed "Submit": they can't press any other button, except for "Refresh". Submit button: "arrow up".
The user pressed "Refresh": go back to 1.
If that's the case, you just need to track two things:
what is the selected answer (when none is selected, nothing can be submitted)
when the answer was submitted - then, the only option is to refresh the question
By the way, your variable buttonCount is pointless: if you have only one variable for the whole view, all buttons will show the same number.
With a good set of ifs and elses, the "Submit" button can be disabled, enabled or show "arrow up", and perform the right action. Here's the code:
// Track which button was pressed - or none of them (starting case)
#State private var buttonPressed: String = ""
// Track if a choice was submitted - when submitted, the Submit button can only refresh
#State private var isWaitingToRefresh = false
// For testing only - tests the refresh
#State private var question = "0"
// (other code)
var body: some View {
// (other code)
Text("getResponsePlaceHolder \(question)")
// (other code)
ScrollView{
VStack { //Buttons
ForEach(buttonData, id: \.mainButtonValue) { data in
// Call a function, no need for binding vars
mainButton(mainButtonText: data.mainButtonText,
mainButtonValue: data.mainButtonValue)
}
} //Button Vstack end
}
submitButton()
}
}
}
func submitButton() -> some View {
Button {
// No action if no button was pressed
guard !buttonPressed.isEmpty else { return }
// Check if you need to refresh the question
if isWaitingToRefresh {
// If the view is waiting to refresh, then refresh the question
refresh()
isWaitingToRefresh = false
} else {
// If the view is not waiting to refresh, submit the user's choice
isWaitingToRefresh = true
getResponse()
}
} label: {
if !isWaitingToRefresh {
Text("Submit")
.font(.system(size: 20))
.foregroundColor(.white)
.padding()
// Gray-out the button if the user made no choice yet
.background(buttonPressed.isEmpty ? Color.secondary : Color.blue)
.cornerRadius(10)
} else {
Image(systemName: "arrow.clockwise.circle")
.font(.system(size: 30))
.foregroundColor(.white)
.padding()
.background(Color.blue)
.cornerRadius(10)
}
}
// Disable the button if the user made no choice yet
.disabled(buttonPressed.isEmpty)
}
// Create main buttons
// Call a function, no need for binding vars:
// you can use the same variables of the view
func mainButton(mainButtonText: String,
mainButtonValue: String) -> some View {
Button {
// You just need to change the choice when a button is pressed,
// let the Submit button do the rest
buttonPressed = mainButtonValue
} label: {
Text(mainButtonText)
Text(String("same number for all buttons")) // testing only
}
.font(.system(size: 20))
.foregroundColor(.white)
.padding()
.background(mainButtonValue == buttonPressed ? Color.teal : Color.purple)
.cornerRadius(10)
.scaleEffect(mainButtonValue == buttonPressed ? 1.12 : 1)
.animation(.easeOut, value: 2)
// Do not let the user change the choice if it was already submitted
.disabled(isWaitingToRefresh)
}
// Remember to reset the user's choice (buttonPressed = "")
func refresh() {
buttonPressed = ""
question = String(Int.random(in: 1...100))
}

fixed text inside TextField SwiftUI

i want to put some fixed text inside TextField like "email: ....."
I tryed this:
HStack {
Image(systemName: "list.bullet").foregroundColor(.gray)
Text("email:")
TextField("", text: Binding(
get: { viewModel.email },
set: { viewModel.email = $0 }
))
.textFieldStyle(DefaultTextFieldStyle())
.frame(width: 60)
}
.padding()
.overlay(RoundedRectangle(cornerRadius: 10).stroke(
viewModel.emailValid() ? Color.gray : Color.red, lineWidth: 1
))
The problem is that this implementation only allows to click in specific part (inside the textField space) but i want to make all clickable to be more friendly to edit.
I tryed putting TextField and Text inside ZStack but the text entered by the user in inside te "Email text".
Maybe its possible to move the textField cursor to the end of the "email" text, or another implementation?
Thanks!
So if its mainly about activating the TextField on tap/click on any area of the element, you can use programmatic focus for TextField:
struct ContentView: View {
#State var input = ""
#FocusState var focused: Bool
var body: some View {
HStack {
Image(systemName: "list.bullet").foregroundColor(.gray)
Text("email:")
TextField("", text: $input)
.frame(width: 80)
.focused($focused)
}
.padding()
.overlay(RoundedRectangle(cornerRadius: 10).stroke(
true ? Color.gray : Color.red, lineWidth: 1
))
.contentShape(Rectangle()) // makes the whole element tappable
.onTapGesture {
focused = true // sets focus for textField
}
}
}

Use TabView to implement inifinite scroll like the WeekView of Calendar app

I am trying to implement a component with a "Horizontal Scroll View" of weekdays similar to the iOS Calendar App. When the user swipe left/right, the dates in scope will be updated to the previous/next week with animation just like the pagers.
I thought the feature is simple enough so I didn't intend to use any third-party framework. I tried TabView to dynamically append items to the beginning/end of the items array, but the animation seems buggy. The problem shows up when the code inserts a new element at the beginning of the array. I'm confused why everything looks good when the element is appended to the last.
Not sure whether it's the right direction I should dig into for this feature. Any suggestions?
struct ContentView: View {
#State private var selection = "2"
#State var items: [String] = ["0", "1", "2", "3", "4"]
var body: some View {
TabView(selection: $selection) {
ForEach(items, id: \.self) { item in
Text(item)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.tag(item)
}
}
.tabViewStyle(PageTabViewStyle())
.indexViewStyle(PageIndexViewStyle(backgroundDisplayMode: .always))
.onChange(of: selection, perform: { value in
if items.firstIndex(of: value) == items.count - 2, let last = items.last, let number = Int(last) {
self.items.append("\(number + 1)")
}
if items.firstIndex(of: value) == 1, let first = items.first, let number = Int(first) {
self.items.insert("\(number - 1)", at: 0)
}
})
// .id(items.count) // <-- This will rerender the list so the animation will be interrupted when count is changed.
}
}

Navigationlink question in Xcode 13.1 -- Blanks instead of links?

Sorry for the newbie question, but I'm stuck on why Navigationlink produces no links at all. Xcode compiles, but there's a blank for where the links to the new views are. This particular view is View 3 from ContentView, so the structure is ContentView -> View 2 -> View 3 (trying to link to View 4).
struct MidnightView: View {
var hourItem: HoursItems
#State var showPreferencesView = false
#State var chosenVersion: Int = 0
#State var isPsalmsExpanded: Bool = false
#State var showLXX: Bool = false
var body: some View {
ScrollView (.vertical) {
VStack (alignment: .center) {
Group {
Text (hourItem.hourDescription)
.font(.headline)
Text ("Introduction to the \(hourItem.hourName)")
.font(.headline)
.bold()
.frame(maxWidth: .infinity, alignment: .center)
ForEach (tenthenou, id: \.self) {
Text ("\($0)")
Text ("\(doxasi)")
.italic()
}
}
.padding()
NavigationView {
List {
ForEach (midnightHours, id:\.id) {watch in
NavigationLink ("The \(watch.watchName)", destination: MidnightWatchView (midnightItem: watch, chosenVersion: self.chosenVersion, isPsalmsExpanded: self.isPsalmsExpanded, showLXX: self.showLXX))
}
}
}
Group {
Text ("Absolution of the \(hourItem.hourName)")
.font(.headline)
Text (absolutionTexts[(hourItem.hourName)] ?? " ")
Divider()
Text ("Conclusion of Every Hour")
.font(.headline)
Text (hourConclusion)
Divider()
Text (ourFather)
}
.padding()
}
}
.navigationBarTitle ("The Midnight Hour", displayMode: .automatic)
.navigationBarItems (trailing: Button (action: {self.showPreferencesView.toggle()}) {Text (psalmVersions[chosenVersion])}
.sheet(isPresented: $showPreferencesView) {PreferencesView(showPreferencesView: self.$showPreferencesView, chosenVersion: self.$chosenVersion, isPsalmsExpanded: self.$isPsalmsExpanded, showLXX: self.$showLXX)})
}
}
The cause of the NavigationLink not appearing is probably due to the fact that you're including a List within a ScrollView. Because List is a scrolling component as well, the sizing gets messed up and the List ends up with a height of 0. Remove the List { and corresponding } and your links should appear.
There are a number of other potential issues in your code (as I alluded to in my comment), including a NavigationView in the middle of a ScrollView. I'd remove that as well, as you probably have a NavigationView higher up in your view hierarchy already.

Editable TextField in SwiftUI List

UPDATE: 14 months later, there is this intriguing note in the AppKit release notes:
A TextField that you are editing inside a selected List row now has correct text foreground colors. (68545878)
Now when placing a TextField in a List, the TextField becomes focused on click the very first time it is selected, but subsequent editing attempts fail: the List row is selected but the TextField does not gain focus.
O/P:
In a beta6 SwiftUI macOS (not iOS) app, I need to have a List with editable text fields, but the TextFields in the list are not editable. It works fine if I swap out List for Form (or just VStack), but I need it working with a List. Is there some trick to tell the list to make the fields editable?
import SwiftUI
struct ContentView: View {
#State var stringField: String = ""
var body: some View {
List { // works if this is VStack or Form
Section(header: Text("Form Header"), footer: Text("Form Footer")) {
TextField("Line 1", text: $stringField).environment(\.isEnabled, true)
TextField("Line 2", text: $stringField)
TextField("Line 3", text: $stringField)
TextField("Line 4", text: $stringField)
TextField("Line 5", text: $stringField)
}
}
}
}
The following code is only an experiment to understand the character of List in SwiftUI and show an alternative. What I understand from observing the output from various combinations is that, List View's style is structured in a way to override the default behaviors of underlying View to become Selectable. This means that TextField does absolutely different. TextField is an focusable element where we can type. This focusing variable is not wired in to List View to work together. Hence, List override default focusable. Hence it is not possible to create List with TextView. But if you need, next best option is ScrollView instead of List and do the styling explicitly. Check the following code and both ways.
import SwiftUI
struct ContentView: View {
#State private var arr = ["1","2","3"]
var body: some View {
HStack {
VStack {
List {
ForEach(self.arr.indices, id:\.self) {
TextField("", text: self.$arr[$0])
}
}
}
.frame(minWidth: 150, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
VStack {
ScrollView {
ForEach(self.arr.indices, id:\.self) {
TextField("", text: self.$arr[$0])
.textFieldStyle(PlainTextFieldStyle())
.padding(2)
}
}
.padding(.leading, 5)
.padding(3)
}
.background(Color(NSColor.alternatingContentBackgroundColors[0]))
.frame(minWidth: 150, maxWidth: .infinity, minHeight: 300, maxHeight: .infinity)
}
}
}
extension NSTextField {
open override var focusRingType: NSFocusRingType {
get { .none }
set { }
}
}
Bug Report
I updated the project to target a MacOS app and found the bug you are reporting. I've updated Apple with this feedback because it indeed does seem to be a bug (Welcome to Beta).
FB7174245 - SwiftUI Textfields embedded in a List are not editable
when target is macOS
Update
So what's the point of all the focus on state and binding below? One variable should be bound to a single control. Here is a working example. I'll leave up the older answer as it carries the example forward with a full app saving and retrieving data to/from CoreData.
import SwiftUI
struct ContentView: View {
#State var field1: String = ""
#State var field2: String = ""
#State var field3: String = ""
#State var field4: String = ""
#State var field5: String = ""
var body: some View {
List { // works if this is VStack or Form
Section(header: Text("Form Header"), footer: Text("Form Footer")) {
TextField("Line 1", text: $field1).environment(\.isEnabled, true)
TextField("Line 2", text: $field2)
TextField("Line 3", text: $field3)
TextField("Line 4", text: $field4)
TextField("Line 5", text: $field5)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
BTW - This works on Xcode Beta (11M392r) with MacOS Catalina - 10.15
Beta (19A546d).
Sample Project
Check out this sample that includes an editable Textfield that writes to CoreData, which I am building on Github.
Take a look at the difference between #State and #Binding so that you
can manipulate data from outside of the content view. (60-sec
video)
struct ContentView: View {
// 1.
#State var name: String = ""
var body: some View {
VStack {
// 2.
TextField(" Enter some text", text: $name)
.border(Color.black)
Text("Text entered:")
// 3.
Text("\(name)")
}
.padding()
.font(.title)
}
}
source
Check out the following answer for the appropriate use-case of #State
(SO Answer)
Does get focus—if you tap just right
Using Xcode 12.4 and SwiftUI 2 for a macOS app, I seemed to have the same problem: Could not make TextEdit work inside a List. After reading here, and experimenting some more, I realized that in my case the TextField does reliably get the focus, but only if you tap in just the right way. I think and hope this is not how it should be working, so I posted the question TextField inside a List in SwiftUI on macOS: Editing not working well, explaining the details of my observations.
In summary: Single-tap exactly on existing text does give focus (after a small, annoying, delay). Double-tap anywhere does not give focus. Single-tap anywhere in an empty field does give focus.
SwiftUI 2.0
You can use TextEditor instead of TextField in lists in order to get editable text fields.
TextEditor(text: $strings)
.font(.body)
.cornerRadius(5)
.shadow(color: Color.black.opacity(0.18), radius: 0.8, y: 1)
.frame(height: 20)
This way you will get a similar aspect between the TextEditor and TextField.