I am trying to create a TextField that spans across the entire window. Currently it only expands to fill the view on the horizontal axis. I'm aiming to achieve something similar to that seen in TextEdit.
Here is my code so far:
struct ContentView: View {
#State var contents: String = "Hello World\nThis is a test.";
var body: some View {
VStack() {
TextField("Notes", text: $contents)
.lineLimit(nil)
}
.frame(height: 600)
}
}
Application Screenshot
Is this possible in SwiftUI or will I need to revert to using legacy AppKit components?
I've found that wrapping an NSTextView is still be the best way to achieve something similar to that seen in TextEdit. So yes, at least now you will have to use legacy AppKit components in your SwiftUI code.
struct MultilineTextView: NSViewRepresentable {
typealias NSViewType = NSTextView
#Binding var text: String
func makeNSView(context: Self.Context) -> Self.NSViewType{
let view = NSTextView()
view.isEditable = true
view.isRulerVisible = true
return view
}
func updateNSView(_ nsView: Self.NSViewType, context: Self.Context) {
nsView.string = text
}
}
struct ContentView: View {
#State var contents: String = "Hello World\nThis is a test.";
var body: some View {
VStack() {
MultilineTextView(text: $contents)
}.background(Color.white)
}
}
It seems, that there is bug with TextField line limit, according to this answer and comments to it.
I believe in this thread you can find answer, how achieve what you want.
Just replying to this as nobody's given a decent answer yet besides wrapping UIKit.
TextEditor(text: $contents)
Is the better path as it WILL behave like you wish as compared to TextField(). It automatically takes care of scrolling and expanding to the View area allowed.
TextEditor WILL require you to be on at least iOS 14. If you're not on iOS 14 then wrapping the UIKit control is your best option.
Related
I want to use a List, #FocusState to track focus, and .onChanged(of: focus) to ensure the currently focused field is visible with ScrollViewReader. The problem is: when everything is setup together the List rebuilds constantly during scrolling making the scrolling not as smooth as it needs to be.
I found out that the List rebuilds on scrolling when I attach .onChanged(of: focus). The issue is gone if I replace List with ScrollView, but I like appearance of List, I need sections support, and I need editing capabilities (e.g. delete, move items), so I need to stick to List view.
I used Self._printChanges() in order to see what makes the body to rebuild itself when scrolling and the output was like:
ContentView: _focus changed.
ContentView: _focus changed.
ContentView: _focus changed.
ContentView: _focus changed.
...
And nothing was printed from the closure attached to .onChanged(of: focus). Below is the simplified example, the smoothness of scrolling is not a problem in this example, however, once the List content is more or less complex the smooth scrolling goes away and this is really due to .onChanged(of: focus) :(
Question: Are there any chances to listen for focus changes and not provoke the List to rebuild itself on scrolling?
struct ContentView: View {
enum Field: Hashable {
case fieldId(Int)
}
#FocusState var focus: Field?
#State var text: String = ""
var body: some View {
List {
let _ = Self._printChanges()
ForEach(0..<100) {
TextField("Enter the text for \($0)", text: $text)
.id(Field.fieldId($0))
.focused($focus, equals: .fieldId($0))
}
}
.onChange(of: focus) { _ in
print("Not printed unless focused manually")
}
}
}
if you add printChanges to the beginning of the body, you can monitor the views and see that they are being rendered by SwiftUI (all of them on each focus lost and focus gained)
...
var body: some View {
let _ = Self._printChanges() // <<< ADD THIS TO SEE RE-RENDER
...
so after allot of testing, it seams that the problem is with .onChange, once you add it SwiftUI will redraw all the Textfields,
the only BYPASS i found is to keep using the deprecated API as it works perfectly, and renders only the two textfields (the one that lost focus, and the one that gained the focus),
so the code should look this:
struct ContentView: View {
enum Field: Hashable {
case fieldId(Int)
}
// #FocusState var focus: Field? /// NO NEED
#State var text: String = ""
var body: some View {
List {
let _ = Self._printChanges()
ForEach(0..<100) {
TextField("Enter the text for \($0)", text: $text)
.id(Field.fieldId($0))
// .focused($focus, equals: .fieldId($0)) /// NO NEED
}
}
// .onChange(of: focus) { _ in /// NO NEED
// print("Not printed unless focused manually") /// NO NEED
// } /// NO NEED
.focusable(true, onFocusChange: { focusNewValue in
print("Only textfileds that lost/gained focus will print this")
})
}
}
I recommend to consider separation of list row content into standalone view and use something like focus "selection" approach. Having FocusState internal of each row prevents parent view from unneeded updates (something like pre-"set up" I assume).
Tested with Xcode 13.4 / iOS 15.5
struct ContentView: View {
enum Field: Hashable {
case fieldId(Int)
}
#State private var inFocus: Field?
var body: some View {
List {
let _ = Self._printChanges()
ForEach(0..<100, id: \.self) {
ExtractedView(i: $0, inFocus: $inFocus)
}
}
.onChange(of: inFocus) { _ in
print("Not printed unless focused manually")
}
}
struct ExtractedView: View {
let i: Int
#Binding var inFocus: Field?
#State private var text: String = ""
#FocusState private var focus: Bool // << internal !!
var body: some View {
TextField("Enter the text for \(i)", text: $text)
.focused($focus)
.id(Field.fieldId(i))
.onChange(of: focus) { _ in
inFocus = .fieldId(i) // << report selection outside
}
}
}
}
As a hobby project, I'm developing a SwiftUI app targeted for macOS.
I have a CoreData entity (let's call it Sample) with a String property called title.
In my main view (SamplesView) I'm displaying a List of Samples, and I want titles be editable directly from the list. For that, I've made a sub-view (SampleRowView) with a TextField, and I'm displaying this sub-view in the List using ForEach.
It works and looks okayish. Though, I can edit the title only if I click directly on the TextField's text (point 1 on the screenshot). If I click on the "empty" part of the TextField (f.e. point 2) it does not respond. I thought that the shape of the TextField is limited somehow by the length of its text, but as visible on the screenshot, TextField occupies the whole row.
Appreciate any help and ideas about how to make the TextField respond to click on its any point, not only on the text.
// "Sample" is a CoreData entity
public class Sample: NSManagedObject {
//...
#NSManaged public var title: String
}
// This is the main view
struct SamplesView: View {
#FetchRequest(...)
var samples: FetchedResults<Sample>
var body: some View {
VStack {
List {
ForEach(samples) { sample in
SampleRowView(sample: sample)
}
.onDelete(perform: deleteSample)
}
}
}
}
// List rows with editable Sample's title
struct SampleRowView: View {
#ObservedObject var sample: Sample
var body: some View {
TextField("", text: $sample.title)
}
}
Update:
The problem is the same even on the fresh project. Also, if I change TextField with TextEditor the behavior is kinda expected.
Digging a bit more into it:
TextField inside a List in SwiftUI on macOS: Editing not working well
Editable TextField in SwiftUI List
SwiftUI make ForEach List row properly clickable for edition in EditMode
I've found that it seems to be a bug in SwiftUI, and for now the only solution is to somehow replace the List with ScrollView with custom item moving and deletion. This is sad.
import SwiftUI
struct Sample: Identifiable {
let id: Int
var title: String
init(id: Int) {
self.id = id
self.title = "Sample \(id)"
}
}
struct TestView: View {
#State var samples = [Sample(id: 1), Sample(id: 2)]
var body: some View {
List {
ForEach($samples) { $sample in
TextField("", text: $sample.title) // .textFieldStyle(.squareBorder) -- doesn't help
// TextEditor(text: $sample.title) // This works as expected
}
}
}
}
#main
struct SampleApp: App {
var body: some Scene {
WindowGroup {
TestView()
}
}
}
I'm using XCode Version 13.2.1, Swift 5, MacOS deployment target 11.6.
You could try to add
.contentShape(Rectangle())
to your View element.
I use it along with Text()-Instances which allows me to accept clicks not only on the written part of the View element, but everywhere within its bounds.
I'm trying to simplify the ContentView within a project and I'm struggling to understand how to move #State based logic into its own file and have ContentView adapt to any changes. Currently I have dynamic views that display themselves based on #Binding actions which I'm passing the $binding down the view hierarchy to have buttons toggle the bool values.
Here's my current attempt. I'm not sure how in SwiftUI to change the view state of SheetPresenter from a nested view without passing the $binding all the way down the view stack. Ideally I'd like it to look like ContentView.overlay(sheetPresenter($isOpen, $present).
Also, I'm learning SwiftUI so if this isn't the best approach please provide guidance.
class SheetPresenter: ObservableObject {
#Published var present: Present = .none
#State var isOpen: Bool = false
enum Present {
case none, login, register
}
#ViewBuilder
func makeView(with presenter: Present) -> some View {
switch presenter {
case .none:
EmptyView()
case .login:
BottomSheetView(isOpen: $isOpen, maxHeight: UIConfig.Utils.screenHeight * 0.75) {
LoginScreen()
}
case .register:
BottomSheetView(isOpen: $isOpen, maxHeight: UIConfig.Utils.screenHeight * 0.75) {
RegisterScreen()
}
}
}
}
if you don't want to pass $binding all the way down the view you can create a StateObject variable in the top view and pass it with .environmentObject(). and access it from any view with EnvironmentObject
struct testApp: App {
#StateObject var s1: sViewModel = sViewModel()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(s1)
}
}
}
You are correct this is not the best approach, however it is a common mistake. In SwiftUI we actually use #State for transient data owned by the view. This means using a value type like a struct, not classes. This is explained at 4:18 in Data Essentials in SwiftUI from WWDC 2020.
EditorConfig can maintain invariants on its properties and be tested
independently. And because EditorConfig is a value type, any change to
a property of EditorConfig, like its progress, is visible as a change
to EditorConfig itself.
struct EditorConfig {
var isEditorPresented = false
var note = ""
var progress: Double = 0
mutating func present(initialProgress: Double) {
progress = initialProgress
note = ""
isEditorPresented = true
}
}
struct BookView: View {
#State private var editorConfig = EditorConfig()
func presentEditor() { editorConfig.present(…) }
var body: some View {
…
Button(action: presentEditor) { … }
…
}
}
Then you just use $editorConfig.isEditorPresented as the boolean binding in .sheet or .overlay.
Worth also taking a look at sheet(item:onDismiss:content:) which makes it much simpler to show an item because no boolean is required it uses an optional #State which you can set to nil to dismiss.
I know there are answers for dismissing keyboard, but mostly they are triggered on tapped outside of the keyboard.
As I stated in the question, how to achieve dismissing keyboard on swipe (to bottom).
UIScrollView has keyboardDismissMode which when set to interactive, will achieve what you want. SwiftUI doesn’t provide direct support for this, but since under the hood, SwiftUI is using UIScrollView, you can use this which sets keyboardDismissMode to interactive for all scroll views in your app.
UIScrollView.appearance().keyboardDismissMode = .interactive
You must have a ScrollView in your view hierarchy for this to work. Here’s is a simple view demonstrating the behavior:
struct ContentView: View {
#State private var text = "Hello, world!"
var body: some View {
ScrollView {
TextField("Hello", text: $text)
.padding()
}
.onAppear {
UIScrollView.appearance().keyboardDismissMode = .interactive
}
}
}
The only caveat is that this affects all scroll views in your app. I don’t know of a simple solution if you only want to affect one scroll view in your app.
For example if you have a list of messages then you can :
List {
ForEach(...) { ...
}
}.resignKeyboardOnDragGesture()
extension View {
func resignKeyboardOnDragGesture() -> some View {
return modifier(ResignKeyboardOnDragGesture())
}
}
struct ResignKeyboardOnDragGesture: ViewModifier {
var gesture = DragGesture().onChanged { _ in
UIApplication.shared.endEditing(true)
}
func body(content: Content) -> some View {
content.gesture(gesture)
}
}
By the way it came from here : https://stackoverflow.com/a/58564739/7974174
You can add
.simultaneousGesture(
// Hide the keyboard on scroll
DragGesture().onChanged { _ in
UIApplication.shared.sendAction(
#selector(UIResponder.resignFirstResponder),
to: nil,
from: nil,
for: nil
)
}
)
to the view.
How can I push a new View on the navigation stack from within a Sheet. I want to display a list of Lessons. When tabbing on one of the lessons, a sheet should open showing details about the lesson. From within the Sheet one should be able to start the lesson in a new fullscreen view.
import SwiftUI
struct ContentView: View {
var lessons = [Lesson(id:"1"), Lesson(id:"2"), Lesson(id:"3"), Lesson(id:"4"), Lesson(id:"5"), Lesson(id:"6"), Lesson(id:"7"), Lesson(id:"8"), Lesson(id:"9")]
var body: some View {
NavigationView(){
Form{
List(lessons){ lesson in
LessonButton(lesson: lesson)
}
}
}
}
}
struct LessonButton:View{
#State var showSheet = false
var lesson:Lesson
var body: some View {
Button(action:{self.showSheet = true}){
Text(lesson.name)
}.sheet(isPresented:$showSheet){
NavigationLink(destination: Text("reached")){
Text("start")
}
}
}
}
struct Lesson: Identifiable{
var id:String
var name: String{
"Lesson \(self.id)"
}
}
However the NavigationLink is not working. I guess, this is because the Sheet is not a ChildView of Content View. That's probably why it does not work. But how can it be achieved?
A bit late, but this question came up while solving this. Your sheet acts like its own view controller stack. You can't navigate the parent through the sheet overlay, nor should you. It does seem like you're asking what I was looking for, which is to emulate other apple apps that navigate in sheets. You simply need an additional NavigationView within your sheet. This will give you a navigation stack to push other sheet styled views to the navigation controller within your first sheet.
(SwiftUI beginner, verbiage is likely wrong)
import SwiftUI
struct NavigateFromSheet: View {
var lessons = [Lesson(id:"1"), Lesson(id:"2"), Lesson(id:"3"), Lesson(id:"4"), Lesson(id:"5"), Lesson(id:"6"), Lesson(id:"7"), Lesson(id:"8"), Lesson(id:"9")]
var body: some View {
NavigationView(){
Form {
List(lessons){ lesson in
LessonButton(lesson: lesson)
}
}
}
}
}
struct LessonButton:View{
#State var showSheet = false
var lesson:Lesson
var body: some View {
Button(action:{self.showSheet = true}){
Text(lesson.name)
}.sheet(isPresented: $showSheet){
NavigationView {
VStack {
Text("My First Sheet")
NavigationLink(destination: Text("reached")){
Text("My Second Sheet")
}
}
}
}
}
}
struct Lesson: Identifiable{
var id:String
var name: String{
"Lesson \(self.id)"
}
}
struct NavigateFromSheet_Previews: PreviewProvider {
static var previews: some View {
NavigateFromSheet()
}
}
Sheet is modal view mode, you can enter in it and return back from it.
Actually I can't understand why do you need a sheet in described scenario. As you described it is expected:
List -> Details -> Lesson,
so use consequently two navigation links, one in List, one in Details. This is a native Apple design for NavigationView/NavigationLink usage - navigation from view to view.