Mimicking behavior of iMessage with TextEditor for text entry - ios

My first ever question here at Stack Overflow.
I'm writing a small application for iOS and macOS. Text entry is done via (now) a TextField and a Button. The issue with the TextField is that it's a single line and doesn't allow for multiline text entry. So, I tried using TextEditor instead, but I can either set it up to not grow as more text is added, or it shows up very big to begin with.
What I'm saying is that ideally, it would mimic the behavior that the text entry in iMessage has: starts as the same size of a TextField but grows if needed to accommodate a multiline text like a TextEditor.
Here's the code I am using for this view:
var inputView: some View {
HStack {
ZStack {
//tried this here...
//TextEditor(text: $taskText)
TextField("New entry...", text: $taskText, onCommit: { didTapAddTask() })
.frame(maxHeight: 35)
.padding(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 0))
.clipped()
.accentColor(.black)
.cornerRadius(8)
.textFieldStyle(RoundedBorderTextFieldStyle())
Text(taskText).opacity(0).padding(.all, 8)
}
Button(action: AddNewEntry, label: { Image(systemName: "plus.circle")
.imageScale(.large)
.foregroundColor(.primary)
.font(.title) }).padding(15).foregroundColor(.primary)
}
}
Any way of doing this?
I tried different approaches found in different questions from other users, but I can't quite figure out.
Here's how it looks:
How the TextEdit looks
I also have tried something like this and played with different values for the .frame:
TextEditor(text: $taskText)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: 200)
.border(Color.primary, width: 1)
.padding(EdgeInsets(top: 0, leading: 12, bottom: 0, trailing: 0))
Any help is appreciated.
Thanks.
Oh, and I'm using Xcode 12.5.1 and target is iOS 14.x and macOS Big Sur for now.
EDIT to answer jnpdx.
When I add the code from Dynamic TextEditor overlapping with other views this is how it looks, and it does not change dynamically.
How it looks when the app launches
How it looks when you type text

Here's an example, using your original code with the Button next to the TextEditor. The TextEditor grows until it hits the limit, defined by maxHeight. It also has a view for the messages (since you mentioned iMessage), but you could easily remove that.
struct ContentView: View {
#State private var textEditorHeight : CGFloat = 100
#State private var text = "Testing text. Hit a few returns to see what happens"
private var maxHeight : CGFloat = 250
var body: some View {
VStack {
VStack {
Text("Messages")
Spacer()
}
Divider()
HStack {
ZStack(alignment: .leading) {
Text(text)
.font(.system(.body))
.foregroundColor(.clear)
.padding(14)
.background(GeometryReader {
Color.clear.preference(key: ViewHeightKey.self,
value: $0.frame(in: .local).size.height)
})
TextEditor(text: $text)
.font(.system(.body))
.padding(6)
.frame(height: min(textEditorHeight, maxHeight))
.background(Color.black)
}
.padding(20)
Button(action: {}) {
Image(systemName: "plus.circle")
.imageScale(.large)
.foregroundColor(.primary)
.font(.title)
}.padding(15).foregroundColor(.primary)
}.onPreferenceChange(ViewHeightKey.self) { textEditorHeight = $0 }
}
}
}
struct ViewHeightKey: PreferenceKey {
static var defaultValue: CGFloat { 0 }
static func reduce(value: inout Value, nextValue: () -> Value) {
value = value + nextValue()
}
}

Related

Animate text on and off screen in SwiftUI

I am trying to animate text to make it scroll across the screen, (using it to make a stock app), I am unable to get it to go completely off the screen can someone please help...
This is what I have so far
let text = "Some text to animate"
private var is = true
var body: some View {
VStack {
Text(text)
.fixedSize()
.frame(width: 100, alignment: is ? .trailing : .leading)
.animation(Animation.linear(duration: 5).repeatForever())
}
A possible solution is to use single Text with .move asymmetric transition.
Here is a simplified demo. Tested with Xcode 13.4 / iOS 15.5
Main part:
var body: some View {
GeometryReader { gp in
VStack {
Text(text)
.fixedSize()
.frame(width: gp.size.width + textWidth, alignment: .trailing)
.id(go)
.transition(transition)
.onAppear{ go.toggle() }
.animation(animation, value: go)
}
}.fixedSize(horizontal: false, vertical: true)
}
Test module on GitHub
like this? It shifts the text using .offset if go is true.
let text = "Some text to animate"
#State private var go = false
var body: some View {
VStack {
Text(text)
.fixedSize()
.frame(width: 100)
.offset(x: go ? 300 : 0, y: 0)
.animation(Animation.linear(duration: 3).repeatForever(), value: go)
.onAppear{self.go.toggle()}
}
}

SwiftUI TextEditor Divider doesn't change Y position based on text-line count?

I am trying to make a SwiftUI TextEditor with a Divider that adapts its position to stay under the bottom-most line of text inside of a edit-bio section of the app.
Note: I have a frame on my TextEditor so that it doesn't take up the whole-screen
Right now the Divider is static and stays in one place. Is there a built-in way to make the divider stay under the bottom most line of text?
I would think the Spacer would have given me this behavior?
Thank you!
struct EditBio: View {
#ObservedObject var editProfileVM: EditProfileViewModel
var body: some View {
VStack(spacing: 10) {
TextEditor(text: $editProfileVM.bio)
.foregroundColor(.white)
.padding(.top, 70)
.padding([.leading, .trailing], 50)
.frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
Divider().frame(height: 1).background(.white)
Spacer()
}
}
}
It is doing exactly what you told it to do. But a background color on your TextEditor. You will see that it has a height of 200 + a spacing of 10 from the VStack.
I changed your code to make it obvious:
struct EditBio: View {
#State var editProfileVM = ""
var body: some View {
VStack(spacing: 10) {
TextEditor(text: $editProfileVM)
.foregroundColor(.white)
.padding(.top, 70)
.padding([.leading, .trailing], 50)
.frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
.background(Color.gray)
Divider().frame(height: 1).background(.red)
Spacer()
}
}
}
to produce this:
You can see the TextEditor naturally wants to be taller than 200, but that is limiting it. Therefore, the Spacer() is not going to cause the TextEditor to be any smaller.
The other problem that setting a fixed frame causes will be that your text will end up off screen at some point. I am presuming what you really want is a self sizing TextEditor that is no larger than it's contents.
That can be simply done with the following code:
struct EditBio: View {
#State var editProfileVM = ""
var body: some View {
VStack(spacing: 10) {
SelfSizingTextEditor(text: $editProfileVM)
// Frame removed for the image below.
// .frame(minWidth: 100, idealWidth: 200, maxWidth: 400, maxHeight: 200, alignment: .center)
.foregroundColor(.white)
// made the .top padding to be .vertical
.padding(.vertical, 70)
.padding([.leading, .trailing], 50)
.background(Color.gray)
Divider().frame(height: 5).background(.red)
Spacer()
}
}
}
struct SelfSizingTextEditor: View {
#Binding var text: String
#State var textEditorSize = CGSize.zero
var body: some View {
ZStack {
Text(text)
.foregroundColor(.clear)
.copySize(to: $textEditorSize)
TextEditor(text: $text)
.frame(height: textEditorSize.height)
}
}
}
extension View {
func readSize(onChange: #escaping (CGSize) -> Void) -> some View {
background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
.onPreferenceChange(SizePreferenceKey.self, perform: onChange)
}
func copySize(to binding: Binding<CGSize>) -> some View {
self.readSize { size in
binding.wrappedValue = size
}
}
}
producing this view:

SwiftUI AccessibilityHidden Still Selectable

Working on all things accessibility related and found that .accessibilityHidden(true) doesn't seem to work correctly. Even though VO doesn't read anything, it still shows the white box as you swipe around the page. The only way I've solved this is to put a VStack around the entire thing and then it seems to be fine, however it truncates the welcomeBodyText in simulator but not on a real device, which is worrying because I need to ensure that this works across all devices.
As the HeaderNav just shows the logo and company name, there is no reason to have it selectable, nor read out on every single page, so I wanted to completely hide it from VO and immediately jump to reading the welcomeHeaderText followed by the welcomeBodyText.
HeaderNav.swift
import SwiftUI
struct HeaderNav: View {
var body: some View {
VStack {
Spacer()
.frame(minHeight: 26, idealHeight: 26, maxHeight: 26)
.fixedSize()
HStack(spacing: 16) {
Spacer()
Image(decorative: "LogoHeader")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 70)
Text(LocalizationStrings.companyName)
.fontWeight(.light)
.foregroundColor(Color("WhiteText"))
.font(.system(size: 34))
.tracking(0.8)
Spacer()
} // End HStack
.accessibilityElement(children: .ignore)
.accessibilityLabel(LocalizationStrings.companyName)
} // End VStack
}
}
WelcomeTextView.swift
import SwiftUI
struct WelcomeTextView: View {
var body: some View {
Section {
VStack { // This is what needed to be added to make it work but causes issues in the simulator
HeaderNav()
.accessibilityHidden(true)
VStack(spacing: 10) {
Group {
HeaderText(text: LocalizationStrings.welcomeHeaderText)
BodyText(text: LocalizationStrings.welcomeBodyText)
} // End Group (Page Description)
.pageDescr()
} // End VStack
} // End VStack to make it work
.accessibilityElement(children: .combine)
} // End Section
.listRowBackground(Color("mainbkg"))
.listRowSeparator(.hidden)
}
}
pageDescr is a View Modifier that looks like:
struct PageDescr: ViewModifier {
func body(content: Content) -> some View {
VStack(alignment: .leading, spacing: 20) {
content
}
.padding(EdgeInsets(top: 5, leading: 32.0, bottom: 25, trailing: 32.0))
}
}
It was the top padding that was causing the issue, so I used a fixed size spacer in the HeaderNav above the closing } of the VStack instead and set the top padding to 0. Now it works.
Like this:
Spacer()
.frame(minHeight: 32, idealHeight: 32, maxHeight: 32)
.fixedSize()

How to extend the width of a button using SwiftUI

I cannot figure out how to change the width of buttons in SwiftUI.
I have already attempted:
using .frame(minWidth: 0, maxWidth: .infinity),
using Spacer() around the button and navigationlink,
using frame on the Text field and padding on the button, look through the documentation, as well as a few other things I found while just searching online. However nothing changes the buttons width whatsoever.
NavigationLink(destination: Home(), isActive: self.$isActive) { Text("") }
Button(action: { self.isActive = true }) { LoginBtn() }
struct LoginBtn: View {
var body: some View {
Text("Login")
.fontWeight(.bold)
.padding()
.foregroundColor(Color.white)
.background(Color.orange)
.cornerRadius(5.0)
}
}
Photo of current button
I would like to have the button to extend to be similar to the width of the TextFields used. Again, I know there have been answers posted but for some reason I cannot get mine to work. Thanks!
Declare your own button style:
struct WideOrangeButton: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding()
.frame(minWidth: 0,
maxWidth: .infinity)
.foregroundColor(.white)
.padding()
.background( RoundedRectangle(cornerRadius: 5.0).fill(Color.orange)
)
}
}
and then use it like this:
Button(action: { self.isActive = true }) {
Text("Login")
.fontWeight(.bold)
} .buttonStyle(WideOrangeButton())
I like this approach since it lets me use the default button style, but still results in a wider button.
Button(action: {
// Whatever button action you want.
}, label: {
Text("Okay")
.frame(maxWidth: .infinity)
})
.buttonStyle(.automatic)

Layout in SwiftUI with horizontal and vertical alignment

I'm trying to accomplish this layout
If I try HStack wrapped in VStack, I get this:
If I try VStack wrapped in HStack, I get this:
Is there a way to baseline align the text with the textfield and get standard spacing from the longest label to the start of the aligned textfields?
not an expert here, but I managed to achieve the desired layout by (1) opting for the 2-VStacks-in-a-HStack alternative, (2) framing the external labels, (3) freeing them from their default vertical expansion constraint by assigning their maxHeight = .infinity and (4) fixing the height of the HStack
struct ContentView: View {
#State var text = ""
let labels = ["Username", "Email", "Password"]
var body: some View {
HStack {
VStack(alignment: .leading) {
ForEach(labels, id: \.self) { label in
Text(label)
.frame(maxHeight: .infinity)
.padding(.bottom, 4)
}
}
VStack {
ForEach(labels, id: \.self) { label in
TextField(label, text: self.$text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
.padding(.leading)
}
.padding(.horizontal)
.fixedSize(horizontal: false, vertical: true)
}
}
Here is the resulting preview:
in order to account for the misaligned baselines of the external and internal labels (a collateral issue that is not related to this specific layout – see for instance this discussion) I manually added the padding
credits to this website for enlightening me on the path to understanding
SwiftUI layout trickeries
SwiftUI Grids to the rescue!
Starting from iOS 16 you should use a Grid for this.
struct ContentView: View {
let labels = ["Username", "Email", "Password"]
var body: some View {
Grid {
ForEach(labels, id: \.self) { label in
GridRow {
Text(label)
TextField(label, text: .constant(""))
}
}
}
}
}
iOS 13-15 legacy hack
You could use kontiki's geometry reader hack for this:
struct Column: View {
#State private var height: CGFloat = 0
#State var text = ""
let spacing: CGFloat = 8
var body: some View {
HStack {
VStack(alignment: .leading, spacing: spacing) {
Group {
Text("Hello world")
Text("Hello Two")
Text("Hello Three")
}.frame(height: height)
}.fixedSize(horizontal: true, vertical: false)
VStack(spacing: spacing) {
TextField("label", text: $text).bindHeight(to: $height)
TextField("label 2", text: $text)
TextField("label 3", text: $text)
}.textFieldStyle(RoundedBorderTextFieldStyle())
}.fixedSize().padding()
}
}
extension View {
func bindHeight(to binding: Binding<CGFloat>) -> some View {
func spacer(with geometry: GeometryProxy) -> some View {
DispatchQueue.main.async { binding.value = geometry.size.height }
return Spacer()
}
return background(GeometryReader(content: spacer))
}
}
We are only reading the height of the first TextField here and applying it three times on the three different Text Views, assuming that all TextFields have the same height. If your three TextFields have different heights or have appearing/disappearing verification labels that affect the individual heights, you can use the same technique but with three different height bindings instead.
Why is this a bit of a hack?
Because this solution will always first render the TextFields without the labels. During this render phase it will set the height of the Text labels and trigger another render. It would be more ideal to render everything in one layout phase.
Looks like this will work:
extension HorizontalAlignment {
private enum MyAlignment: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> Length {
context[.trailing]
}
}
static let myAlignmentGuide = HorizontalAlignment(MyAlignment.self)
}
struct ContentView : View {
#State var username: String = ""
#State var email: String = ""
#State var password: String = ""
var body: some View {
VStack(alignment: .myAlignmentGuide) {
HStack {
Text("Username").alignmentGuide(.myAlignmentGuide, computeValue: { d in d[.trailing] })
TextField($username)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 200)
}
HStack {
Text("Email")
.alignmentGuide(.myAlignmentGuide, computeValue: { d in d[.trailing] })
TextField($email)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 200)
}
HStack {
Text("Password")
.alignmentGuide(.myAlignmentGuide, computeValue: { d in d[.trailing] })
TextField($password)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 200)
}
}
}
}
With that code, I am able to achieve this layout:
The caveat here is that I had to specify a max width for the TextFields. Left unconstrained, the layout system described in the WWDC talk I linked in the comments retrieves a size for the TextField prior to alignment happening, causing the TextField for email to extend past the end of the other two. I'm not sure how to address this in a way that will allow the TextFields to expand to the size of the containing view without going over...
var body: some View {
VStack {
HStack {
Text("Username")
Spacer()
TextField($username)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 200)
.foregroundColor(.gray)
.accentColor(.red)
}
.padding(.horizontal, 20)
HStack {
Text("Email")
Spacer()
TextField($email)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 200)
.foregroundColor(.gray)
}
.padding(.horizontal, 20)
HStack {
Text("Password")
Spacer()
TextField($password)
.textFieldStyle(.roundedBorder)
.frame(maxWidth: 200)
.foregroundColor(.gray)
}
.padding(.horizontal, 20)
}
}
You need to add fixed width and leading alignment. I've tested in Xcode 11.1 it's ok.
struct TextInputWithLabelDemo: View {
#State var text = ""
let labels = ["Username", "Email", "Password"]
var body: some View {
VStack {
ForEach(labels, id: \.self) { label in
HStack {
Text(label).frame(width: 100, alignment: .leading)
TextField(label, text: self.$text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
.padding(.horizontal)
.fixedSize(horizontal: false, vertical: true)
}
}
}
Below You can see what the issue when we use different VStack for Text and TextField. See more info here
Updated 16 Oct 2019
A closer inspection of Texts and TextFields you can notice that they have different heights and it effects the positions of Texts relative to TextFields as you can see on the right side of the screenshot that Password Text is higher relative to Password TextField than the Username Text relative to Username TextField.
I gave three ways to resolve this issue here
HStack{
Image(model.image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 10, alignment: .leading)
VStack(alignment: .leading) {
Text("Second column ")
Text("Second column -")
}
Spacer()
Text("3rd column")
}
1- first column - image
2- second column - two text
3- the float value
Spacer() - Play with Spacer() -> above example image and vstack remains together vertical align for all rows, just put spacer for the views you want to do in another vertical alignment / column
VStack(alignment: .leading. - this is importent to make alignment start

Resources