SwiftUI ScrollView messing up picker selection behaviour - swiftui

I'm building a WatchOS-app (SwiftUI) with multiple pickers, but as soon as I add them to a ScrollView I can no longer simply tap a picker to select it.
When I tap a picker the first picker on the screen gets selected and I have to tap once more to have the right picker selected.
Once I've double tapped the picker I can select other pickers just fine, but as soon as I tap outside to deselect all pickers I have to double tap again.
Sorry if the explanation is a bit fuzzy. This video shows the issue: Video
I'm new to both programming and Swift, so be gentle ;)
import SwiftUI
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ContentView: View {
let paceArray = Array(0...59)
let speedArray = Array(0...99)
#State private var globalSecondsPerKM: Double = 0
#State private var paceMKMHours: Int = 0
#State private var paceMKMMinutes: Int = 0
#State private var paceMKMSeconds: Int = 0
#State private var paceMMHours: Int = 0
#State private var paceMMMinutes: Int = 0
#State private var paceMMSeconds: Int = 0
#State private var speedKMHWhole: Int = 0
#State private var speedKMHDecimal: Int = 0
#FocusState private var paceMKMFocused: Bool
#FocusState private var paceMMFocused: Bool
#FocusState private var speedKMHFocused: Bool
var body: some View {
ScrollView {
VStack {
VStack {
Text("Pace per km")
.font(.headline)
HStack {
Picker(selection: $paceMKMHours, label: Text(""), content: {
ForEach(0..<speedArray.count, id: \.self) { index in
Text(String(format: "%02dh", speedArray[index])).tag(index)
}
})
.frame(width: 45)
Picker(selection: $paceMKMMinutes, label: Text(""), content: {
ForEach(0..<paceArray.count, id: \.self) { index in
Text(String(format: "%02dm", paceArray[index])).tag(index)
}
})
.frame(width: 45)
Picker(selection: $paceMKMSeconds, label: Text(""), content: {
ForEach(0..<paceArray.count, id: \.self) { index in
Text(String(format: "%02ds", paceArray[index])).tag(index)
}
})
.frame(width: 45)
}
.focused($paceMKMFocused)
.padding(.bottom, 5)
.frame(height: 35)
}
Divider()
VStack {
Text("Pace per mile")
.font(.headline)
HStack {
Picker(selection: $paceMMHours, label: Text(""), content: {
ForEach(0..<speedArray.count, id: \.self) { index in
Text(String(format: "%02dh", speedArray[index])).tag(index)
}
})
.frame(width: 45)
Picker(selection: $paceMMMinutes, label: Text(""), content: {
ForEach(0..<paceArray.count, id: \.self) { index in
Text(String(format: "%02dm", paceArray[index])).tag(index)
}
})
.frame(width: 45)
Picker(selection: $paceMMSeconds, label: Text(""), content: {
ForEach(0..<paceArray.count, id: \.self) { index in
Text(String(format: "%02ds", paceArray[index])).tag(index)
}
})
.frame(width: 45)
}
.focused($paceMMFocused)
.padding(.bottom, 5)
.frame(height: 35)
}
Divider()
VStack {
Text("Speed in km/h")
.font(.headline)
HStack {
Picker(selection: $speedKMHWhole, label: Text(""), content: {
ForEach(0..<speedArray.count, id: \.self) { index in
Text(String(format: "%02d", speedArray[index])).tag(index)
}
})
.frame(width: 45)
Picker(selection: $speedKMHDecimal, label: Text("")) {
ForEach(0..<speedArray.count, id: \.self) { index in
Text(String(format: ".%02d", speedArray[index])).tag(index)
}
}
.frame(width: 45)
}
.focused($speedKMHFocused)
.padding(.bottom, 5)
.frame(height: 35)
}
Divider()
}
}
.labelsHidden()
.font(.system(size: 13))
}
}

This looks like a SwiftUI bug. A possible workaround is setting up a tap gesture on the picker, which triggers a focus change. The initial animation is not perfect, but it looks fine after that.
import SwiftUI
#available(watchOSApplicationExtension 8.0, *)
struct ContentView: View {
let paceArray = Array(0...59)
#State private var paceMKMHours: Int?
#State private var paceMKMMinutes: Int?
#State private var paceMKMSeconds: Int?
#FocusState private var shouldFocusHours: Bool
#FocusState private var shouldFocusMinutes: Bool
#FocusState private var shouldFocusSeconds: Bool
var body: some View {
ScrollView {
VStack {
HStack {
Picker("hours", selection: $paceMKMHours, content: {
ForEach(0..<paceArray.count, id: \.self) { index in
Text(String(format: "%02dh", paceArray[index])).tag(index)
}
})
.onTapGesture {
shouldFocusHours = true
}
.focused($shouldFocusHours)
Picker("minutes", selection: $paceMKMMinutes, content: {
ForEach(0..<paceArray.count, id: \.self) { index in
Text(String(format: "%02dm", paceArray[index])).tag(index)
}
})
.onTapGesture {
shouldFocusMinutes = true
}
.focused($shouldFocusMinutes)
Picker("seconds", selection: $paceMKMSeconds, content: {
ForEach(0..<paceArray.count, id: \.self) { index in
Text(String(format: "%02ds", paceArray[index])).tag(index)
}
})
.onTapGesture {
shouldFocusSeconds = true
}
.focused($shouldFocusSeconds)
}
.padding(.bottom, 5)
.frame(height: 35)
}
}
.labelsHidden()
.font(.system(size: 13))
}
}

Related

How to have a Navigation link open a different view?

I keep trying to have the navigation links in the surveys list in contentView to open detailView when selected. I keep getting errors Missing argument for parameter 'newsurvey' in call Insert 'newsurvey: <#Survey#>'
Here's the home view
struct ContentView: View {
#State var surveys: [Survey] = []
#State var isPresented = false
#State var selectedTab: Int = 0
#State private var path = [String]()
let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateFormat = "MM/dd/yyyy"
return formatter
}()
var body: some View {
ZStack {
VStack{
NavigationView {
VStack {
Text("")
.frame(height: 0)
.navigationBarTitleDisplayMode(.inline)
//reduces navbartitle height
.toolbar {
ToolbarItem(placement: .principal) {
Image("RR_Logo_Primary_Full_Color")
.resizable()
.frame(width: 275.0, height: 55.0)
}
}
List(surveys) { survey in
NavigationLink(destination: DetailView(newsurvey: survey)) //must be within navigationview
{
HStack{
ZStack {
Rectangle()
.frame(width: 400, height: 100.0)
.foregroundColor(.white)
.cornerRadius(4)
HStack (alignment: .top) {
VStack (alignment: .leading) {
Text(survey.customerName)
.offset(x: -60, y: -2)
.font(.custom("Roboto-Light", size: 24))
Text(survey.lineName)
.offset(x: -60, y: -5)
.font(.custom("Roboto-Light", size: 18))
Text("# of conveyors")
.offset(x: -60)
}
VStack {
Text(dateFormatter.string(from: survey.date))
.font(.custom("Roboto-Light", size: 18))
.offset(x: 60)
}
}
}
}
}
}
.listRowInsets(EdgeInsets())
}
}
.environment(\.defaultMinListHeaderHeight, 1)
CustomTabBar(surveys: $surveys, isPresented: $isPresented)
.frame(height: 60, alignment: .bottom)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I tried the following code below but I keep getting not in scope error.
#State var newsurvey: Survey
#State var surveys: [Survey] = []
#State private var conveyorName = ""
#State private var masterTag = ""
#State private var showAddConveyorSheet = false
#State private var conveyors: [Conveyor] = []
var body: some View {
NavigationView {
VStack {
List {
ForEach(conveyors, id: \.name) { conveyor in
NavigationLink(destination: ConveyorTrack(conveyor: conveyor)) {
Text(conveyor.name)
}
}
}
Button(action: {
self.showAddConveyorSheet = true
}) {
Text("Add Conveyor")
}
}
.navigationBarTitle("Conveyors", displayMode: .inline)
.sheet(isPresented: $showAddConveyorSheet) {
VStack {
HStack {
TextField("Conveyor Name", text: $conveyorName)
TextField("Master Tag", text: $masterTag)
.onTapGesture {
// open camera and scan text here
}
}
Button(action: {
self.conveyors.append(Conveyor(id: self.conveyorName, name: self.conveyorName, masterTag: self.masterTag))
self.conveyorName = ""
self.masterTag = ""
self.showAddConveyorSheet = false
}) {
Text("Save")
}
}
}
}
}
}
struct DetailView_Previews: PreviewProvider {
static var previews: some View {
DetailView(newsurvey: survey)
}
}

Dropdown menu button SwiftUI

I'm trying to implement such dropdown menu https://imgur.com/a/3KcKhv4 but could do it like that https://imgur.com/67bKU5Q
The problem is that selected option doesn't have to repeated. Could you please help me how can I do dropdown menu like in design?
class MenuViewModel: ObservableObject {
#Published var selectedOption: String = "За все время"
}
struct DropdDown: View {
let buttons = ["За все время", "За день", "За неделю"]
#ObservedObject var viewModel = MenuViewModel()
#State var expanded: Bool = false
var body: some View {
VStack(spacing: 30) {
Button {
self.expanded.toggle()
} label: {
Text(viewModel.selectedOption)
.fontWeight(.bold)
.foregroundColor(Color.black)
Spacer()
Image(systemName: "chevron.down")
.foregroundColor(Color.white)
}
if expanded {
ForEach(self.buttons, id: \.self) { buttonTitle in
VStack(alignment: .leading, spacing: 5) {
Button {
self.expanded.toggle()
viewModel.selectedOption = buttonTitle
} label: {
Text(buttonTitle)
.padding(10)
}
.foregroundColor(Color.black)
}
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
.padding()
.frame(width: 300)
.background(Color.gray)
.cornerRadius(10)
}
}
struct DropdDown_Previews: PreviewProvider {
static var previews: some View {
DropdDown()
}
}
Just create computed property array in DropdDown View for store buttons without selectedOption
var availableButtons: [String] {
return buttons.filter { $0 != viewModel.selectedOption }
}
And use in ForEach loop instead buttons array
ForEach(self.availableButtons, id: \.self) {}

Can't select Picker

I am trying to make a stock control system in SwiftUI where users can add an ingredient and then ad an amount and then select a unit of measurement(kg, g, l ,ml).
The app allows them to click a button which allows them to add an ingredient to a text-box which is created and then their input is added to a list.
I am having trouble allowing the user to also type in a number in a text-box next to the ingredient text-box and making the picker clickable
Here is my code
import SwiftUI
struct UploadView2: View {
#State var ingredients = [String]()
#State var amount = [String]()
#State var choices = ["g", "kg", "ml", "l"]
#State var choosen: String = ""
#EnvironmentObject var viewRouter: ViewRouter
func getBinding(forIndex index: Int) -> Binding<String> {
return Binding<String>(get: { ingredients[index] },
set: { ingredients[index] = $0 })
}
var body: some View {
VStack{
HStack{
Button {
print("Going Back")
viewRouter.currentPage = .UploadView
} label: {
Image(systemName: "arrow.left")
.font(.system(size: 30))
.foregroundColor(.black)
}
.padding(.horizontal)
Spacer()
Text("Add Ingredients")
.font(.system(size: 30))
.fontWeight(.bold)
Spacer()
Button {
print("Saved")
} label: {
Image(systemName: "bookmark")
.font(.system(size: 30))
.foregroundColor(.black)
}
.padding()
}
Form {
ForEach(0..<ingredients.count, id: \.self) { index in
HStack {
Button(action: { ingredients.remove(at: index) }) {
Image(systemName: "minus.circle.fill")
.foregroundColor(.red)
.padding(.horizontal)
}
TextField("Ingredient", text: getBinding(forIndex: index))
Picker("", selection: $choosen){
ForEach(choices, id: \.self) { i in
Text("\(i)").tag(i)
}
}
}
}
Button(action: { ingredients.append("") }) {
HStack {
Image(systemName: "plus")
.foregroundColor(.black)
.padding(.horizontal)
Text("add an ingredient")
.foregroundColor(.black)
}
}
}
Button {
//change view router
//add data to data class
viewRouter.currentPage = .UploadView3
} label: {
Label("next", systemImage: "arrow.right")
}
.padding()
.frame(width: 100)
.foregroundColor(Color.white)
.background(Color.red)
.cornerRadius(8)
}
}
}
struct UploadView2_Previews: PreviewProvider {
static var previews: some View {
UploadView2()
.previewDevice(PreviewDevice(rawValue: "iPhone 13"))
.previewInterfaceOrientation(.portrait)
UploadView2()
.previewDevice(PreviewDevice(rawValue: "iPhone 8"))
}
}
When i click on my click it removes the text-box like the dismiss button
My overall goal is to have a text-box to enter an ingredient and then a text-box to enter an amount and then a picker to select a unit of measurement.
How can I achieve this using my current code as much as I can?
to be able to "select" your Picker, you could try this approach, as shown in this example code:
struct ContentView: View {
#State var ingredient = ""
#State var amount = ""
#State var choosen = ""
#State var choices = ["g", "kg", "ml", "l"]
var body: some View {
HStack {
TextField("Ingredient", text: $ingredient) // <-- or your getBinding thing
TextField("Amount", text: $amount)
Picker("", selection: $choosen){
ForEach(choices, id: \.self) { i in
Text("\(i)").tag(i)
}
}
.pickerStyle(.inline)
.frame(width: 55)
.clipped() // <-- here
}
}
}

Why the scrollview doesn't get updated with new data from array?

I'm trying to send and then display them in the scrollview realtime. But nothing shows up. How to solve it? So, basically when the user types the message into a textbox then it will be saved in array and then it will be populated to the crollView in realtime so the user can view all the messages.
Error: No errors, it just isn't visible.
import SwiftUI
struct SingleMessageBubbleModel: Identifiable {
let id = UUID()
var text: String
var received: Bool
var timeStamp: Date
}
var messagesDBArray : [SingleMessageBubbleModel] = []
struct ContentView: View {
#State private var showOnTheSpotMessaging: Bool = true
#State var textTyped: String
var body: some View {
if (showOnTheSpotMessaging) {
VStack {
HStack {
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(messagesDBArray, id: \.id) { message in
MessageBubble(message: message)
}
}
}
.padding(.top, 10)
.background(.gray)
.onChange(of: messagesDBArray.count) { id in
withAnimation {
proxy.scrollTo(id, anchor: .bottom)
}
}
}
.frame( height: 200, alignment: .bottomLeading)
}
HStack () {
TextEditor (text: $textTyped)
.frame(width: 200, height: 200, alignment: .leading)
Button ("Send", action: {
messagesDBArray.append(SingleMessageBubbleModel(text: textTyped, received: true, timeStamp: Date()))
})
}
}
}
}
}
struct MessageBubble: View {
var message: SingleMessageBubbleModel
#State private var showTime = false
var body: some View {
VStack(alignment: message.received ? .leading : .trailing) {
HStack {
Text(message.text)
.padding()
.background(message.received ? Color.gray : Color.blue)
.cornerRadius(30)
}
.frame(maxWidth: 300, alignment: message.received ? .leading : .trailing)
.onTapGesture {
withAnimation {
showTime.toggle()
}
}
if showTime {
Text("\(message.timeStamp.formatted(.dateTime.hour().minute()))")
.font(.caption2)
.foregroundColor(.gray)
.padding(message.received ? .leading : .trailing, 25)
}
}
.frame(maxWidth: .infinity, alignment: message.received ? .leading : .trailing)
.padding(message.received ? .leading : .trailing)
.padding(.horizontal, 4)
}
}
Basically, when the button is pressed, your property messagesDBArray is well and truly append with the new value.
However, and it's really important to understand this point in swiftUI, nothing triggers the refresh of the view.
I suggest you two solutions:
If you don't need messagesDBArray to be outside of ContentView:
You just have to add messagesDBArray as a state in ContentView like following
struct ContentView: View {
#State var messagesDBArray : [SingleMessageBubbleModel] = []
#State private var showOnTheSpotMessaging: Bool = true
#State var textTyped: String = ""
var body: some View {
if (showOnTheSpotMessaging) {
VStack {
HStack {
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(messagesDBArray, id: \.id) { message in
MessageBubble(message: message)
}
}
}
.padding(.top, 10)
.background(.gray)
.onChange(of: messagesDBArray.count) { id in
withAnimation {
proxy.scrollTo(id, anchor: .bottom)
}
}
}
.frame( height: 200, alignment: .bottomLeading)
}
HStack () {
TextEditor (text: $textTyped)
.frame(width: 200, height: 200, alignment: .leading)
Button ("Send", action: {
messagesDBArray.append(SingleMessageBubbleModel(text: textTyped, received: true, timeStamp: Date()))
})
}
}
}
}
}
If you need messagesDBArray to be outside of ContentView:
1- Create a class (ViewModel or Service or whatever you wan to call it) with messagesDBArray as a #Published property
final class ViewModel: ObservableObject {
#Published var messagesDBArray : [SingleMessageBubbleModel] = []
}
2- Observe this class in ContentView in order to append and receive the update
struct ContentView: View {
#ObservedObject private var viewModel = ViewModel()
#State private var showOnTheSpotMessaging: Bool = true
#State var textTyped: String = ""
var body: some View {
if (showOnTheSpotMessaging) {
VStack {
HStack {
ScrollViewReader { proxy in
ScrollView {
LazyVStack {
ForEach(viewModel.messagesDBArray, id: \.id) { message in
MessageBubble(message: message)
}
}
}
.padding(.top, 10)
.background(.gray)
.onChange(of: viewModel.messagesDBArray.count) { id in
withAnimation {
proxy.scrollTo(id, anchor: .bottom)
}
}
}
.frame( height: 200, alignment: .bottomLeading)
}
HStack () {
TextEditor (text: $textTyped)
.frame(width: 200, height: 200, alignment: .leading)
Button ("Send", action: {
viewModel.messagesDBArray.append(SingleMessageBubbleModel(text: textTyped, received: true, timeStamp: Date()))
})
}
}
}
}
}
I hope that this is clear to you and that it has been useful 😉

Result of 'View' initializer is unused

My Custom button does not tap and passes to next view called AddCreditCardView.
I have tested the button action with print statement and it won't work too.
I copied my code below in separate.
This is my ContentView
import SwiftUI
struct ContentView: View {
let membershipRows = MembershipData.listData()
let corporateRows = CorporateData.listData()
let otherOperationRows = OtherOperationsData.listData()
#State var selectedCard = CreditCard(id: "", cardOwnerName: "", cardNumber: "", cardExpMonth: "", cardExpYear: "", ccv: "")
#State var shown: Bool = false
var body: some View {
NavigationView {
VStack {
List {
Section(header: Text("Bireysel")) {
ForEach(membershipRows) { row in
NavigationLink(destination: CreditCardView()) {
RowElementView(row: row)
}
}
}
if self.corporateRows.count == 0
{
Rectangle()
.background(Color(.white))
.edgesIgnoringSafeArea(.all)
.foregroundColor(.white)
.padding(.vertical,32)
}
else {
Section(header: Text("Kurumsal")) {
ForEach(corporateRows) { row in
RowElementView(row: row)
}
}
}
Section(header: Text("Diger Islemler")) {
ForEach(otherOperationRows) { row in
RowElementView(row: row)
}
}
Rectangle()
.foregroundColor(.clear)
.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height )
}
.navigationBarTitle("Odeme Yontemleri", displayMode: .inline)
.font(Font.custom("SFCompactDisplay", size: 16))
Button(action: {
AddCreditCardView(item: self.selectedCard)
}, label: { CustomButton(title: "Odeme Yontemi Ekle", icon: .none, status: .enable)
})
}
}
}
This is my AddCreditCardView
import SwiftUI
struct AddCreditCardView: View {
var item: CreditCard
var body: some View {
NavigationView {
VStack {
TopBar()
Spacer()
CardInfo()
Spacer()
}
.navigationBarTitle("Odeme Yontemi", displayMode: .inline)
}
}
}
struct TopBar : View {
var body: some View {
VStack {
HStack() {
Image("addcreditcard")
Image("line")
Image("locationBar")
Image("line")
Image("check-circle")
}
.padding(.horizontal,62)
VStack {
Text("Kredi Karti Ekle")
.font(Font.custom("SFCompactDisplay-Bold", size: 14))
Text("1. Adim")
.font(Font.custom("SFCompactDisplay", size: 14))
.fontWeight(.regular)
.foregroundColor(.gray)
}
}
.padding()
}
}
struct CardInfo : View {
var body: some View {
VStack {
CustomTextField(tFtext: "Kartin Uzerindeki Isim", tFImage: "user")
.textContentType(.givenName)
CustomTextField(tFtext: "Kredi Kart Numarasi", tFImage: "credit")
.textContentType(.oneTimeCode)
.keyboardType(.numberPad)
HStack {
CreditCardDateTextField(tFtext: "", tFImage: "date")
.textContentType(.creditCardNumber)
Spacer()
Text("|")
.foregroundColor(.black)
.overlay(
Rectangle()
.frame(width: 60, height: 53))
CustomTextField(tFtext: "CCV", tFImage: "")
.textContentType(.creditCardNumber)
}
.foregroundColor(Color(#colorLiteral(red: 0.9647058824, green: 0.9725490196, blue: 0.9882352941, alpha: 1)))
CustomTextField(tFtext: "Kart Ismi", tFImage: "cardEdit")
Spacer()
}
}
}
And Finally, this is my CreditCard Model
import SwiftUI
struct CreditCard: Identifiable {
var id: String = UUID().uuidString
var cardOwnerName : String
var cardNumber: String
var cardExpMonth: String
var cardExpYear: String
var ccv: String
Seems like you are trying to navigate to AddCreditCardView on the button press. The action closure can not present a view automatically like that! You should change that code to something like this:
#State var navigated = false
,,,
NavigationLink("AddCreditCardView", destination: AddCreditCardView(), isActive: $navigated)
Button(action: { self.navigated.toggle() },
label: { CustomButton(title: "Odeme Yontemi Ekle", icon: .none, status: .enable) })
changing the navigated state will show the next page as it seems you wished.