Text gets truncated or hidden on a TextField on a macOS app - macos

I have a TextField on a .sheet that is acting up and I don't know why or how to solve it. When you type a long text, the cursor reaches the end of the TextField (the right end), and it stays there. You can continue to write (and the text will get entered) but you can't see it, nor scroll to it, nor move the cursor.
Is this a default behavior? If not, how do I fix it? What am I not doing right? I haven't found anything either here at SO, or on Apple's forums or various SwiftUI sites.
Here's how it looks
And here's the code for the view:
struct SheetViewNew: View {
#EnvironmentObject private var taskdata: DataModel
#Binding var isVisible: Bool
#Binding var enteredText: String
#Binding var priority: Int
var priorities = [0, 1, 2]
var body: some View {
VStack {
Text("Enter a new task")
.font(.headline)
.multilineTextAlignment(.center)
TextField("New task", text: $enteredText)
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(.system(size: 14.0))
.onSubmit{
if enteredText == "" {
self.isVisible = false
return
}
taskdata.tasks.append(Task(id: UUID(), title: enteredText, priority: priority))
}
Picker("Priority", selection: $priority) {
Text("Today").tag(0)
Text("Important").tag(1)
Text("Normal").tag(2)
}.pickerStyle(RadioGroupPickerStyle())
Spacer()
HStack {
Button("Cancel") {
self.isVisible = false
self.enteredText = ""
}
.keyboardShortcut(.cancelAction)
.buttonStyle(DefaultButtonStyle())
Spacer()
Button("Add Task") {
if enteredText == "" {
self.isVisible = false
return
}
taskdata.tasks.append(Task(id: UUID(), title: enteredText, priority: priority))
taskdata.sortList()
self.isVisible = false
}
.keyboardShortcut(.defaultAction)
.buttonStyle(DefaultButtonStyle())
}
}
.frame(width: 300, height: 200)
.padding()
}
}

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)
})
}
}

Select the first item on a list and highlight it when item changes

I have a simple notes app that uses a list on the left and a texteditor on the right. The list has the titles of the notes, and the texteditor its text. When the user changes the text on the note, the list gets sorted to display the most current note (by date) on top, as its first item.
Started with Ventura, if I'm working on a note other than the first one, then that note (the item in the list) jumps to the top when the text is changed, however, if the first item in the list not visible (I'm working on a note that is way down), then when I change the text the item, it jumps to the top, but you don't jump with it. You are now in this state where you have to scroll up to get to the top and reselect the first item.
I tried using DispatchQueue.main.async to force to reselect the item onchange, but regardless of what I try, it doesn't scroll to the top, even when the selected note id is the correct one.
I ran out of ideas or things to try. How can I go back to the first item once the text is changed?
Here's the code:
struct NoteItem: Codable, Hashable, Identifiable {
let id: Int
var text: String
var date = Date()
var dateText: String {
dateFormatter.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return dateFormatter.string(from: date)
}
var tags: [String] = []
}
final class DataModel: ObservableObject {
#AppStorage("notes") public var notes: [NoteItem] = []
}
struct AllNotes: View {
#EnvironmentObject private var data: DataModel
#State var noteText: String = ""
#State var selectedNoteId: UUID?
var body: some View {
NavigationView {
List(data.notes) { note in
NavigationLink(
destination: NoteView(note: note),
tag: note.id,
selection: $selectedNoteId
) {
VStack(alignment: .leading) {
Text(note.text.components(separatedBy: NSCharacterSet.newlines).first!)
Text(note.dateText).font(.body).fontWeight(.light)
}
.padding(.vertical, 8)
}
}
.listStyle(InsetListStyle())
}
.navigationTitle("A title")
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: {
data.notes.append(NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []))
}) {
Image(systemName: "square.and.pencil")
}
}
}
.onAppear {
DispatchQueue.main.async {
selectedNoteId = data.notes.first?.id
}
}
.onChange(of: data.notes) { notes in
if selectedNoteId == nil || !notes.contains(where: { $0.id == selectedNoteId }) {
selectedNoteId = data.notes.first?.id
}
}
}
}
struct NoteView: View {
#EnvironmentObject private var data: DataModel
var note: NoteItem
#State var text: String = ""
var body: some View {
HStack {
VStack(alignment: .leading) {
TextEditor(text: $text).padding().font(.body)
.onChange(of: text, perform: { value in
guard let index = data.notes.firstIndex(of: note) else { return }
data.notes[index].text = value
data.sortList()
})
Spacer()
}
Spacer()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}

Select the first item on a list and highlight it when the application launches

When the app first launches in macOS (Big Sur), it populates a list with the items saved by the user. When the user clicks on an item on that list, a second view opens up displaying the contents of that item.
Is there a way to select the first item on that list, as if the user clicked it, and display the second view when the app launches? Furthermore, if I delete an item on the list, I can't go back and select the first item on the list and displaying the second view for that item, or if I create new item, same applies, can't select it.
I have tried looking at answers here, like this, and this, and looked and tried code from a variety of places, but I can't get this to work.
So, using the code answered on my previous question, here's how the bare bones app looks like:
struct NoteItem: Codable, Hashable, Identifiable {
let id: Int
var text: String
var date = Date()
var dateText: String {
dateFormatter.dateFormat = "EEEE, MMM d yyyy, h:mm a"
return dateFormatter.string(from: date)
}
var tags: [String] = []
}
final class DataModel: ObservableObject {
#AppStorage("notes") public var notes: [NoteItem] = []
}
struct AllNotes: View {
#EnvironmentObject private var data: DataModel
#State var noteText: String = ""
var body: some View {
NavigationView {
List(data.notes) { note in
NavigationLink(destination: NoteView(note: note)) {
VStack(alignment: .leading) {
Text(note.text.components(separatedBy: NSCharacterSet.newlines).first!)
Text(note.dateText).font(.body).fontWeight(.light)
}
.padding(.vertical, 8)
}
}
.listStyle(InsetListStyle())
Text("Select a note...")
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.navigationTitle("A title")
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: {
data.notes.append(NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []))
}) {
Image(systemName: "square.and.pencil")
}
}
}
}
}
struct NoteView: View {
#EnvironmentObject private var data: DataModel
var note: NoteItem
#State var text: String = ""
var body: some View {
HStack {
VStack(alignment: .leading) {
TextEditor(text: $text).padding().font(.body)
.onChange(of: text, perform: { value in
guard let index = data.notes.firstIndex(of: note) else { return }
data.notes[index].text = value
})
Spacer()
}
Spacer()
}
.padding()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.white)
.onAppear() {
print(data.notes.count)
}
}
}
I have tried adding #State var selection: Int? in AllNotes and then changing the list to
List(data.notes, selection: $selection)
and trying with that, but I can't get it to select anything.
Sorry, newbie here on SwiftUI and trying to learn.
Thank you!
You were close. Table view with selection is more about selecting item inside table view, but you need to select NavigationLink to be opened
There's an other initializer to which does exactly what you need. To selection you pass current selected item. To tag you pass current list item, if it's the same as selection, NavigationLink will open
Also you need to store selectedNoteId instead of selectedNote, because this value wouldn't change after your update note properties
Here I'm setting selectedNoteId to first item in onAppear. You had to use DispatchQueue.main.async hack here, probably a NavigationLink bug
To track items when they get removed you can use onChange modifier, this will be called each time passed value is not the same as in previous render
struct AllNotes: View {
#EnvironmentObject private var data: DataModel
#State var noteText: String = ""
#State var selectedNoteId: UUID?
var body: some View {
NavigationView {
List(data.notes) { note in
NavigationLink(
destination: NoteView(note: note),
tag: note.id,
selection: $selectedNoteId
) {
VStack(alignment: .leading) {
Text(note.text.components(separatedBy: NSCharacterSet.newlines).first!)
Text(note.dateText).font(.body).fontWeight(.light)
}
.padding(.vertical, 8)
}
}
.listStyle(InsetListStyle())
}
.navigationTitle("A title")
.toolbar {
ToolbarItem(placement: .navigation) {
Button(action: {
data.notes.append(NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []))
}) {
Image(systemName: "square.and.pencil")
}
}
}
.onAppear {
DispatchQueue.main.async {
selectedNoteId = data.notes.first?.id
}
}
.onChange(of: data.notes) { notes in
if selectedNoteId == nil || !notes.contains(where: { $0.id == selectedNoteId }) {
selectedNoteId = data.notes.first?.id
}
}
}
}
Not sure what's with #AppStorage("notes"), it shouldn't work because this annotation only applied to simple types. If you wanna store your items in user defaults you had to do it by hand.
After removing it, you were missing #Published, that's why it wasn't updating in my case. If AppStorage could work, it may work without #Published
final class DataModel: ObservableObject {
#Published
public var notes: [NoteItem] = [
NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []),
NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []),
NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []),
NoteItem(id: UUID(), text: "New Note", date: Date(), tags: []),
]
}

SwiftUI Animations in the same view are displayed differently

In the first section there is no problem, but in the second section there is a problem in my animation. using the same view in the first section and the second section.
In the second section, the text shifts to the right and left. But I don't want that. I want the text to remain stable just like in the first section.
SectionView:
(option == 1) = image on the left, text on the right.
(option == 2) = image on the right, text on the left.
struct SectionView: View {
var title: String
var imageView: AnyView
var description: String
var option: Int
var body: some View {
Group {
if option == 1 {
HStack {
ZStack {
Rectangle()
.frame(width: 125, height: 120)
.foregroundColor(Color(UIColor.secondarySystemBackground))
.cornerRadius(40, corners: [.topRight, .bottomRight])
imageView
}
Spacer()
VStack {
Text(title)
.font(.system(size: 14, weight: .bold, design: .rounded))
Text(description)
.multilineTextAlignment(.center)
.font(.system(size: 12, weight: .medium, design: .rounded))
.padding(.trailing, 5)
}
.frame(height: 120)
.foregroundColor(Color(UIColor.label))
}
} else if option == 2 {
HStack {
VStack {
Text(title)
.font(.system(size: 14, weight: .bold, design: .rounded))
Text(description)
.multilineTextAlignment(.center)
.font(.system(size: 12, weight: .medium, design: .rounded))
.padding(.leading, 5)
}
.frame(height: 120)
.foregroundColor(Color(UIColor.label))
Spacer()
ZStack {
Rectangle()
.frame(width: 125, height: 120)
.foregroundColor(Color(UIColor.secondarySystemBackground))
.cornerRadius(40, corners: [.topLeft, .bottomLeft])
imageView
}
}
}
}
}
}
MainViewModel:
I am adding sections here.
As you can see I'm using the same view in the two CategorySections I've added. In the first section, the animation is displayed properly, but while the animation is displayed in the second section, the text shifts to the right and left. What is the reason for this?
class MainViewModel: ObservableObject {
#Published var section: [CategorySection] = []
init() {
getSections()
}
func getSections() {
section = [
CategorySection(title: "Trafik Levhaları", imageView: AnyView(PoliceSignsSectionIconView()), description: "Trafik levhaları trafiğe çıkan her sürücünün mutlaka dikkat etmesi gereken uyarı işaretleridir. Trafik kurallarını ve gerekliliklerini uygulama açısından önemli bir yeri olan trafik işaretleri mutlaka izlenmeli. Bu bölümde trafik levha işaretlerinin anlamlarını öğrenebilirsiniz.", option: 1, page: TrafficSignsView()),
CategorySection(title: "Polis İşaretleri", imageView: AnyView(PoliceSignsSectionIconView()), description: "Trafik polisleri gerektiği durumlarda yolda trafiğin kontrolünü sağlar. Çoğu yerde karşımıza çıkabilecek bu durumda trafik polisi işaretlerinin anlamını bilmemiz gerekmektedir. Bu bölümde polis işaretlerinin anlamlarını öğrenebilirsiniz.", option: 2, page: PoliceSignsView()),
....
]
}
}
CategorySection Model:
struct CategorySection {
let id = UUID()
let title: String
let imageView: AnyView
let description: String
let option: Int
let page: Any
}
Using SectionView:
I am using the sections I added in MainViewModel here.
struct MainView: View {
#ObservedObject var mainViewModel: MainViewModel = MainViewModel()
#EnvironmentObject var publishObjects: PublishObjects
var body: some View {
NavigationView {
ScrollView(showsIndicators: false) {
VStack(spacing: 30) {
ForEach(mainViewModel.section, id: \.id) { section in
NavigationLink(
destination: AnyView(_fromValue: section.page)?.navigationBarTitleDisplayMode(.inline),
label: {
SectionView(title: section.title, imageView: section.imageView, description: section.description, option: section.option)
})
}
}
}
.navigationBarTitle("Ehliyet Sınavım")
.onAppear {
}
}
.accentColor(Color(UIColor.label))
}
}
PoliceSignsSectionIconView(Animation here):
I applied my animations on this page.
struct PoliceSignsSectionIconView: View {
#State var isDrawing: Bool = true
let pathBounds = UIBezierPath.calculateBounds(paths: [.policePath1, .policePath2, .policePath3, .policePath4, .policePath5, .policePath6, .policePath7, .policePath8, .policePath9, .policePath10])
var body: some View {
Text("Test Icon")
.frame(width: 75, height: 70)
.opacity(isDrawing ? 1 : 0)
.onAppear {
self.isDrawing.toggle()
}
.animation(.easeInOut(duration: 2).repeatForever(autoreverses: true))
}
}
I solved my problem. I don't know why my problem was solved when I added List object.
MainView:
struct MainView: View {
#ObservedObject var mainViewModel: MainViewModel = MainViewModel()
#EnvironmentObject var publishObjects: PublishObjects
var body: some View {
NavigationView {
List(mainViewModel.section, id: \.id) { section in
NavigationLink(
destination: AnyView(_fromValue: section.page)?.navigationBarTitleDisplayMode(.inline),
label: {
SectionView(title: section.title, imageView: section.imageView, description: section.description, sectionStatus: section.sectionStatus)
})
}
.navigationBarTitle("Ehliyet Sınavım")
}
.accentColor(Color(UIColor.label))
}
}

SwiftUI How to add done button to picker

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.

Resources