SwiftUI: How do I make TextField fit multi-line content? - ios

In the attached code example I get a lot of extra top-spacing in my TextField. If I change the content to only be a single line, say "content", then it fits snugly. How can I get the same tight-fitting behaviour the single line has for a multi-line text?
Previews and code were made with Xcode 11.1 / Swift 5.1
import SwiftUI
struct TextFieldDemo: View {
var content: Binding<String>
init(content: Binding<String>) {
self.content = content
}
var body: some View {
TextField("Custom placeholder", text: content)
.background(Color.yellow)
}
}
#if DEBUG
struct TextInputRowPreviews: PreviewProvider {
static var previews: some View {
let content = "content\ncontent\ncontent\ncontent\ncontent\ncontent"
return TextFieldDemo(content: .constant(content))
.previewLayout(.sizeThatFits)
}
}
#endif
Here is the example if I change the "let content" line to
let content = "content"

It seems there's no direct argument to manage multiline padding correctly. They are maybe underdevelopping. But the following will give you a straight workaround solution to what you are expecting.
extension String{
var extraLines : String{ get{
return self + String(repeating:"\n", count: self.components(separatedBy: "\n").count - 1)
}}
}
struct TextFieldDemo: View {
var content: Binding<String>
init(content: Binding<String>) {
self.content = content
}
#State var height : CGFloat? //current height
let constHeightRatio : CGFloat = 0.55 //use for assembly with other fonts.
let defaultHeight : CGFloat = 250 //use for assembly with other views.
var body: some View {
TextField("Custom placeholder", text: content).environment(\.multilineTextAlignment, .center).alignmentGuide(.bottom) { (ViewDimensions) -> CGFloat in
if self.height == nil {self.height = ViewDimensions.height}
return ViewDimensions.height
}.frame( height: (height ?? defaultHeight) * constHeightRatio, alignment: .bottom).background(Color.yellow)
}
}
#if DEBUG
struct TextInputRowPreviews: PreviewProvider {
static var previews: some View {
let content = "content\ncontent\ncontent".extraLines
return
TextFieldDemo(content: .constant(content))
}
}
#endif
This works fine for single view. If view assembly is required (with other stacking views, etc), you may adjust defaultHeight and/or constHeightRatio to achieve what you want. Hopefully it works for you too.

Related

SwiftUI: Combining Custom Text

With the SwiftUI built-in Text we are able to concatenate multiple Text views if we want to create a rich Text
like this:
Text("hello").foregroundColor(.white) + Text("world").foregroundColor(.pink)
However, if I have a custom Text:
struct MyText: View {
let label: String
var body: some View {
Text(self.label)
.foregroundColor(.myAppColor)
}
}
and then combine:
MyText(label: "hello") + MyText(label: "world")
the compiler outputs the following error:
Referencing operator function '+' on 'FloatingPoint' requires that 'MyText' conform to 'FloatingPoint'
I tried casting MyText to Text but the compiler doesn't like that either.
How do I go about achieving this in a slick manner?
This is a custom feature of Text, you can, however, mimick some of the behaviour. See the following example:
import SwiftUI
struct MyText: View {
private let configurations: [Configuration]
init(_ title: String, foregroundColor: Color = .black) {
self.configurations = [
.init(title: title,
foregroundColor: foregroundColor)
]
}
private init(configurations: [Configuration]) {
self.configurations = configurations
}
private struct Configuration: Identifiable {
let id = UUID()
let title: String
let foregroundColor: Color
}
var body: some View {
HStack {
ForEach(configurations, content: Render.init)
}
}
static func + (lhs: Self, rhs: Self) -> Self {
let configurations = lhs.configurations + rhs.configurations
return MyText(configurations: configurations)
}
private struct Render: View {
let configuration: Configuration
var body: some View {
Text(configuration.title)
.foregroundColor(configuration.foregroundColor)
}
}
}
struct MyText_Previews: PreviewProvider {
static var previews: some View {
MyText("hej") + MyText("Yo", foregroundColor: .red)
}
}
This is much more a proof of concept, than an advice to do this practice however.

how to set a View as a parameter in SwiftUI

I am trying to provide an option through a Configuration struct to show a custom view. I am not sure how to accomplish this. I tried a number of options but I keep getting compile errors. May be my approach to do this is wrong! I appreciate your help!
Here is what I tried:
configuration struct that provides options to set color, width and custom content, etc...
struct Configuration {
var color: Color
....
var customView: (([String])->AnyView?) = nil // <- this compiles but not sure if this is the right way to do it!
...
}
defining my custom view:
struct CustomView: View {
var values: [String]
var body: some View {
VStack {
ForEach(values.indices, id:\.self) { index in
Text(values[index])
}
}
}
}
Here is how I am using it and where I get the compiler error:
struct ContentView: View {
var config = Configuration()
config.customView = CustomView(["A", "B"]) // <-- Error: Cannot assign value of type CustomView to type ([String]) -> AnyView
var body: some View {
VStack {
.... // display other content
}
.overlay(showCustomView())
}
#ViewBuilder
private func showCustomView() -> some View {
if let customContent = config.customView {
customContent()
} else {
EmptyView()
}
}
}
Here is possible solution (as far as I understood your intention) - you need generics in configuration to be able to use it with any view.
Here is a demo. Prepared with Xcode 12.4.
struct Configuration<V: View> { // << view dependent
var color: Color
var customView: (([String]) -> V?)? = nil // << view builder
}
struct CustomView: View { // no changes
var values: [String]
var body: some View {
VStack {
ForEach(values.indices, id:\.self) { index in
Text(values[index])
}
}
}
}
struct ContentView: View {
var config = Configuration(color: .black) { // << create config !!
CustomView(values: $0) // ... and specify view !!
}
var body: some View {
VStack {
Text("")
}
.overlay(showCustomView())
}
#ViewBuilder
private func showCustomView() -> some View {
if let customContent = config.customView {
customContent(["A", "B"]) // << inject params !!
} else {
EmptyView()
}
}
}

SwiftUI DidBecomeActiveNotification - all values are lost

I am new to iOS, and I am developing my fairy tales application with SwiftUI. Everything is ok, but I found one error - when entering background and returning back to the app, all data gets lost from my View.
I tried to debug the app, and I see that when receiving willEnterForegroundNotification, the data still exists. But, when receiving didBecomeActiveNotification, data already gets lost.
Could anyone help me to understand, how to preserve data in the View, when entering back foreground?
import SwiftUI
import DynamicColor
struct FairyTaleStoryView: View, FairyModelDelegate, WebImageLoadProtocol {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
#State private var offset = CGSize.zero // for gesture recognition - how far the user has dragged
let screenWidth = UIScreen.main.bounds.size.width
let screenHeight = UIScreen.main.bounds.size.height
let storyId : Int
#State var pos : Int = 0
#ObservedObject var networkManager = NetworkManager()
var musicManager = MusicManager()
var states = FairyStates()
#State var textShowing = 1
.....
var storyPresenter : some View {
....
}
var body: some View {
VStack {
storyPresenter
.navigationBarTitle("")
.navigationBarHidden(true)
.onAppear(perform: {
self.networkManager.delegate = self
self.networkManager.fetchFairyTale(id: storyId)
})
}
.background(Color(hexString: Colors.BackgroundColor))
.edgesIgnoringSafeArea(.all)
.onDisappear() {
stopSound()
stopMusic()
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
print(networkManager.fairyTale) // prints an object
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
print(networkManager.fairyTale) // prints empty object
}
}
}
struct FairyTaleStoryView_Previews: PreviewProvider {
static var previews: some View {
FairyTaleStoryView(storyId: 0)
}
}

How to set textfield character limit SwiftUI?

I'm using SwiftUi version 2 for my application development. I'm facing issue with textfield available in SwiftUI. I don't want to use UITextField anymore. I want to limit the number of Characters in TextField. I searched a lot and i find some answer related to this but those answer doesn't work for SwiftUI version 2.
class textBindingManager: ObservableObject{
let characterLimit: Int
#Published var phoneNumber = "" {
didSet {
if phoneNumber.count > characterLimit && oldValue.count <= characterLimit {
phoneNumber = oldValue
}
}
}
init(limit: Int = 10) {
characterLimit = limit
}
}
struct ContentView: View {
#ObservedObject var textBindingManager = TextBindingManager(limit: 5)
var body: some View {
TextField("Placeholder", text: $textBindingManager.phoneNumber)
}
}
No need to use didSet on your published property. You can add a modifier to TextField and limit the string value to its prefix limited to the character limit:
import SwiftUI
struct ContentView: View {
#ObservedObject var textBindingManager = TextBindingManager(limit: 5)
var body: some View {
TextField("Placeholder", text: $textBindingManager.phoneNumber)
.padding()
.onChange(of: textBindingManager.phoneNumber, perform: editingChanged)
}
func editingChanged(_ value: String) {
textBindingManager.phoneNumber = String(value.prefix(textBindingManager.characterLimit))
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
class TextBindingManager: ObservableObject {
let characterLimit: Int
#Published var phoneNumber = ""
init(limit: Int = 10){
characterLimit = limit
}
}
The following should be the simpliest. It limits the number of characters to 10.
struct ContentView: View {
#State var searchKey: String = ""
var body: some View {
TextField("Enter text", text: $searchKey)
.onChange(of: searchKey) { newValue in
if newValue.count > 10 {
self.searchKey = String(newValue.prefix(10))
}
}
}
}
This solution wraps everything up in a new Component. You could adapt this to perform other parsing / pattern checking quite easily.
struct ContentView : View {
#State private var myTextValue: String = ""
var body: some View {
LimitedTextField(value: $myTextValue, charLimit: 2)
}
}
struct LimitedTextField : View {
#State private var enteredString: String = ""
#Binding var underlyingString: String
let charLimit : Int
init(value: Binding<String>, charLimit: Int) {
_underlyingString = value
self.charLimit = charLimit
}
var body: some View {
HStack {
TextField("", text: $enteredString, onCommit: updateUnderlyingValue)
.onAppear(perform: { updateEnteredString(newUnderlyingString: underlyingString) })
.onChange(of: enteredString, perform: updateUndelyingString)
.onChange(of: underlyingString, perform: updateEnteredString)
}
}
func updateEnteredString(newUnderlyingString: String) {
enteredString = String(newUnderlyingString.prefix(charLimit))
}
func updateUndelyingString(newEnteredString: String) {
if newEnteredString.count > charLimit {
self.enteredString = String(newEnteredString.prefix(charLimit))
underlyingString = self.enteredString
}
}
func updateUnderlyingValue() {
underlyingString = enteredString
}
}

How to make a SwiftUI text field extend vertically when the the edge is reached? [duplicate]

In the attached code example I get a lot of extra top-spacing in my TextField. If I change the content to only be a single line, say "content", then it fits snugly. How can I get the same tight-fitting behaviour the single line has for a multi-line text?
Previews and code were made with Xcode 11.1 / Swift 5.1
import SwiftUI
struct TextFieldDemo: View {
var content: Binding<String>
init(content: Binding<String>) {
self.content = content
}
var body: some View {
TextField("Custom placeholder", text: content)
.background(Color.yellow)
}
}
#if DEBUG
struct TextInputRowPreviews: PreviewProvider {
static var previews: some View {
let content = "content\ncontent\ncontent\ncontent\ncontent\ncontent"
return TextFieldDemo(content: .constant(content))
.previewLayout(.sizeThatFits)
}
}
#endif
Here is the example if I change the "let content" line to
let content = "content"
It seems there's no direct argument to manage multiline padding correctly. They are maybe underdevelopping. But the following will give you a straight workaround solution to what you are expecting.
extension String{
var extraLines : String{ get{
return self + String(repeating:"\n", count: self.components(separatedBy: "\n").count - 1)
}}
}
struct TextFieldDemo: View {
var content: Binding<String>
init(content: Binding<String>) {
self.content = content
}
#State var height : CGFloat? //current height
let constHeightRatio : CGFloat = 0.55 //use for assembly with other fonts.
let defaultHeight : CGFloat = 250 //use for assembly with other views.
var body: some View {
TextField("Custom placeholder", text: content).environment(\.multilineTextAlignment, .center).alignmentGuide(.bottom) { (ViewDimensions) -> CGFloat in
if self.height == nil {self.height = ViewDimensions.height}
return ViewDimensions.height
}.frame( height: (height ?? defaultHeight) * constHeightRatio, alignment: .bottom).background(Color.yellow)
}
}
#if DEBUG
struct TextInputRowPreviews: PreviewProvider {
static var previews: some View {
let content = "content\ncontent\ncontent".extraLines
return
TextFieldDemo(content: .constant(content))
}
}
#endif
This works fine for single view. If view assembly is required (with other stacking views, etc), you may adjust defaultHeight and/or constHeightRatio to achieve what you want. Hopefully it works for you too.

Resources