Make TextEditor dynamic height SwiftUI - string

I'm trying to create a growing TextEditor as input for a chat view.
The goal is to have a box which expands until 6 lines are reached for example. After that it should be scrollable.
I already managed to do this with strings, which contain line breaks \n.
TextEditor(text: $composedMessage)
.onChange(of: self.composedMessage, perform: { value in
withAnimation(.easeInOut(duration: 0.1), {
if (value.numberOfLines() < 6) {
height = startHeight + CGFloat((value.numberOfLines() * 20))
}
if value.numberOfLines() == 0 || value.isEmpty {
height = 50
}
})
})
I created a string extension which returns the number of line breaks by calling string.numberOfLines() var startHeight: CGFloat = 50
The problem: If I paste a text which contains a really long text, it's not expanding when this string has no line breaks. The text get's broken in the TextEditor.
How can I count the number of breaks the TextEditor makes and put a new line character at that position?

Here's a solution adapted from question and answer,
struct ChatView: View {
#State var text: String = ""
// initial height
#State var height: CGFloat = 30
var body: some View {
ZStack(alignment: .topLeading) {
Text("Placeholder")
.foregroundColor(.appLightGray)
.font(Font.custom(CustomFont.sofiaProMedium, size: 13.5))
.padding(.horizontal, 4)
.padding(.vertical, 9)
.opacity(text.isEmpty ? 1 : 0)
TextEditor(text: $text)
.foregroundColor(.appBlack)
.font(Font.custom(CustomFont.sofiaProMedium, size: 14))
.frame(height: height)
.opacity(text.isEmpty ? 0.25 : 1)
.onChange(of: self.text, perform: { value in
withAnimation(.easeInOut(duration: 0.1), {
if (value.numberOfLines() < 6) {
// new height
height = 120
}
if value.numberOfLines() == 0 || value.isEmpty {
// initial height
height = 30
}
})
})
}
.padding(4)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.appLightGray, lineWidth: 0.5)
)
}
}
And the extension,
extension String {
func numberOfLines() -> Int {
return self.numberOfOccurrencesOf(string: "\n") + 1
}
func numberOfOccurrencesOf(string: String) -> Int {
return self.components(separatedBy:string).count - 1
}
}

I found a solution!
For everyone else trying to solve this:
I added a Text with the same width of the input field and then used a GeometryReader to calculate the height of the Text which automatically wraps. Then if you divide the height by the font size you get the number of lines.
You can make the text field hidden (tested in iOS 14 and iOS 15 beta 3)

If you're looking for an iOS 15 solution, I spent a while and figured it out. I didn't want to have to resort to UIKit or ZStacks with overlays or duplicative content as a "hack". I wanted it to be pure SwiftUI.
I ended up creating a separate struct that I could reuse anywhere I needed it, as well as add additional parameters in my various views.
Here's the struct:
struct FieldMultiEntryTextDynamic: View {
var text: Binding<String>
var body: some View {
TextEditor(text: text)
.padding(.vertical, -8)
.padding(.horizontal, -4)
.frame(minHeight: 0, maxHeight: 300)
.font(.custom("HelveticaNeue", size: 17, relativeTo: .headline))
.foregroundColor(.primary)
.dynamicTypeSize(.medium ... .xxLarge)
.fixedSize(horizontal: false, vertical: true)
} // End Var Body
} // End Struct
The cool thing about this is that you can have placeholder text via an if statement and it supports dynamic type sizes.
You can implement it as follows:
struct MyView: View {
#FocusState private var isFocused: Bool
#State private var myName: String = ""
var body: some View {
HStack(alignment: .top) {
Text("Name:")
ZStack(alignment: .trailing) {
if myName.isEmpty && !isFocused {
Text("Type Your Name")
.font(.custom("HelveticaNeue", size: 17, relativeTo: .headline))
.foregroundColor(.secondary)
}
HStack {
VStack(alignment: .trailing, spacing: 5) {
FieldMultiEntryTextDynamic(text: $myName)
.multilineTextAlignment(.trailing)
.keyboardType(.alphabet)
.focused($isFocused)
}
}
}
}
.padding()
.background(.blue)
}
}
Hope it helps!

Related

How to scale multiline text by gesture in SwiftUI

I need to implement multiline text scaling by gesture. I tried to use scaleEffect() but it's become blurry and pixelized (first gif). Then I tried to change font directly from gesture but faced another problem: text starts jumping, changing line breaks and etc (second gif). fixedSize() can't solve this because it's multiline text.
I need to implement scaling like with scaleEffect() but without blurring like with scaling font directly. Here's my code:
struct ContentView: View {
#State var activeTextScale: CGFloat = 1.0
#State var lastScale: CGFloat = 1.0
#State var fontSize: CGFloat = 24
var body: some View {
ZStack {
ZStack {
Text(
"Abcdefghijklmnopqrstuvwxyz Abcdefghijklmnopqrstuvwxyz Abcdefghijklmnopqrstuvwxyz"
)
.foregroundColor(.yellow)
// .scaleEffect(activeTextScale)
.font(.system(size: fontSize * activeTextScale))
.background(
Rectangle()
.foregroundColor(.brown.opacity(0.6))
// .scaleEffect(activeTextScale)
)
.gesture(
MagnificationGesture()
.onChanged { value in
activeTextScale = value + lastScale
}
.onEnded { value in
lastScale = activeTextScale
}
)
}
}
}
}
Here I'm tried to scale font and view accordingly, but it gives the same result as on second gif:
struct ContentView: View {
#State var activeTextScale: CGFloat = 1.0
#State var lastScale: CGFloat = 1.0
#State var fontSize: CGFloat = 24
func getTextFontSize() -> CGFloat {
let size = fontSize * activeTextScale
print(activeTextScale)
print(size.rounded())
return size.rounded()
}
var body: some View {
ZStack {
VStack {
Text(
"Abcdefghijklmnopqrstuvwxyz Abcdefghijklmnopqrstuvwxyz Abcdefghijklmnopqrstuvwxyz"
)
.foregroundColor(.yellow)
.font(.system(size: getTextFontSize()))
.background(
Rectangle()
.foregroundColor(.brown.opacity(0.6))
)
}
.scaleEffect(activeTextScale)
.gesture(
MagnificationGesture()
.onChanged { value in
activeTextScale = value + lastScale
}
.onEnded { value in
lastScale = activeTextScale
}
)
}
}
}
First gif with scaleEffect()
Second gif with font scaling
Third gif with scaling view and font accordingly

Core Data, Problem with updating (duplicating instead)

I am new to Swift UI. Could you please help me with core data updating?
Here is the point of a problem:
I am building a WatchOS app. There are 3 Views there:
FirstView - a view with a button to Add a new Goal and a List of added Goals.
AddGoalView - appears after pressing Add new Goal.
RingView - a view with a Goal Ring (similar to activity ring mechanics) and all the data presented.
The point of the problem is the next:
After adding a new Goal everything is alright. The Data passes correctly from AddGoalView to the FirstView. I need only 2 items to be passed out of AddGoalView (One String and one Double).
Then, after pressing on the recently created Goal I appear on the Ring View. I successfully pass there 2 items (that String and Double I mentioned).
On the RingView I want to update the 3-rd Item (double) and send it back. So it can be updated on the FirstView.
the Result:
Instead of updating this 3-d Item it just seems to create a completely new Goal on the First View below the previous Goal. Photo
My Code (FirstView):
struct FirstView: View {
#FetchRequest (
entity:NewGoal.entity(),
sortDescriptors:[NSSortDescriptor(keyPath: \NewGoal.dateAdded, ascending: false)],
animation: .easeInOut )
var results:FetchedResults<NewGoal>
#State var showMe = false
var body: some View {
ScrollView{
VStack{
VStack(alignment: .leading){
Text("My Goals:")
NavigationLink(
destination: AddGoalView(),
isActive: $showMe,
label: {
Image(systemName: "plus")
Text("Set Money Goal")
})
Text("Recents:")
ForEach(results){ item in
VStack(alignment: .leading){
NavigationLink(
destination: RingView(GTitle: item.goalTitle ?? "", Sum: item.neededSum),
label: {
HStack{
Image(systemName: "gear")
VStack(alignment: .leading){
Text(item.goalTitle ?? "")
HStack{
Text("$\(item.yourSum, specifier: "%.f")") ///This item doesn't update
Text("/ $\(item.neededSum, specifier: "%.f")")
}
}
}
})
}
}
}
}
}
}
}
My Code (AddGoalView):
struct AddGoalView: View {
#State private var goalTitle = ""
#State private var showMe:Bool = true
#State private var neededSum:Double = 0.0
#State private var isFocusedNum = false
#Environment(\.managedObjectContext) var context
#Environment(\.presentationMode) var presentationMode
var body: some View {
ScrollView{
VStack (alignment: .leading, spacing: 6){
TextField("Goal Name...", text: $goalTitle)
HStack{
Text("$\(neededSum, specifier: "%.f")")
.overlay(
RoundedRectangle(cornerRadius: 9)
.stroke(isFocusedNum ? Color.red : Color.white, lineWidth: 1)
.opacity(1.0))
.focusable(true) { newState in isFocusedNum = newState}
.animation(.easeInOut(duration: 0.1), value: isFocusedNum)
.digitalCrownRotation(
$neededSum,
from: 0,
through: 100000,
by: 25,
sensitivity: .high)
}
Button(action: addGoal) {
Text("Add Goal")
}
.disabled(neededSum == 0.0)
.disabled(goalTitle == "")
.navigationTitle("Edit")
}
}
}
private func addGoal(){
let goal = NewGoal(context: context)
goal.goalTitle = goalTitle
goal.dateAdded = Date()
goal.neededSum = neededSum
do{
try context.save()
presentationMode.wrappedValue.dismiss()
}catch let err{
print(err.localizedDescription)
}
}
My Code (RingView Code):
struct RingView: View {
#State private var isFocusedSum = false
#State private var yournewSum:Double = 0.0
var goalItem: NewGoal?
var Sum:Double
var GTitle:String
#Environment(\.managedObjectContext) var context
#Environment(\.presentationMode) var presentationMode
#FetchRequest var results: FetchedResults<NewGoal>
init(GTitle: String, Sum: Double){
self.GTitle = GTitle
self.Sum = Sum
let predicate = NSPredicate(format:"goalTitle == %#", GTitle)
self._results=FetchRequest(
entity: NewGoal.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \NewGoal.dateAdded, ascending: false)],
predicate: predicate,
animation: .easeInOut
)
}
var body: some View {
ZStack{
ForEach(results) { item in
RingShape(percent:(yournewSum/item.neededSum*100), startAngle: -90, drawnClockwise: false) /// Ring
.stroke(style: StrokeStyle(lineWidth: 10, lineCap: .round))
.fill(AngularGradient(gradient: Gradient(colors: [.red, .pink, .red]), center: .center))
.frame(width: 155, height: 155)
HStack(alignment: .top){
Spacer()
Button(action: addSum) { ///BUTTON TO Update
Image(systemName: "gear")
}
.clipShape(Circle())
}
VStack(alignment: .trailing, spacing: 0.0){
Spacer()
Text("$\(yournewSum, specifier: "%.f")") /// Here is the data I want to change via Digital Crown and update
.font(.title3)
.overlay(
RoundedRectangle(cornerRadius: 7)
.stroke(Color.white, lineWidth: 2)
.opacity(isFocusedSum ? 1.0:0.0)
)
.focusable(true) { newState in isFocusedSum = newState}
.animation(.easeInOut(duration: 0.3), value: isFocusedSum)
.digitalCrownRotation(
$yournewSum,
from: 0,
through: Double((item.neededSum)),
by: 10,
sensitivity: .high)
Text("/ $\(item.neededSum, specifier: "%.f")") ///Here is the Double data I entered in AddGoalView
.font(.caption)
}
.frame(width: 200, height: 230)
.padding(.top, 7)
VStack(alignment: .center, spacing: 1.0){
Text(item.goalTitle ?? "Your Goal Name") ///Here is the String data I entered in AddGoalView
.foregroundColor(.gray)
}
.padding(.top, 200.0)
}
}
.padding([.top, .leading, .trailing], 5.0)
}
private func addSum(){
let goal = goalItem == nil ? NewGoal(context: context): goalItem
goal?.yourSum = yournewSum //// I am trying to update the Data here, but after running the func it creates a duplicate.
do{
try context.save()
presentationMode.wrappedValue.dismiss()
} catch let err{
print(err.localizedDescription)
}
}
You never give var goalItem: NewGoal? the initial value of the item you want to update.
try replacing this
RingView(GTitle: item.goalTitle ?? "", Sum: item.neededSum)
with
RingView(goalItem: item, GTitle: item.goalTitle ?? "", Sum: item.neededSum)
and of course your have to change your initializer for RingView to
init(goalItem: NewGoal? = nil, GTitle: String, Sum: Double){
and add to the initializer this line
self.goalItem = goalItem

Core Data Count SwiftUI

When I add a new reminder to my list I can not see the count + 1 simultaneously. But when I re-run the program I see the count is correct.
https://vimeo.com/545025225
struct ListCell: View {
var list : CDListModel
#State var count = 0
#State var isSelected: Bool = false
var body: some View {
HStack{
Color(list.color ?? "")
.frame(width: 30, height: 30, alignment: .center)
.cornerRadius(15)
Text(list.text ?? "")
.foregroundColor(.black)
.font(.system(size: 20, weight: .regular, design: .rounded))
Spacer()
Text(String(count))
.foregroundColor(.gray)
.onAppear{
DispatchQueue.main.async {
self.count = list.reminders!.count
}
}
}
}
}
If CDListModel is a CoreData entity, then you can just add this:
#ObservedObject var list : CDListModel
Also remove the State for the count.
Then display the count like this:
Text(String(list.reminders!.count))
As hint: I wouldn't use force unwrapping aswell. Maybe provide a default value instead of force unwrapping

SwiftUI layout with GeometryReader inside ScrollView

I'm trying to create a scrollable grid of items. I create a custom view called GridView which uses GeometryReader to divide the space into columns and rows in HStacks and VStacks. But for some reason, its size shrinks to almost nothing when inside a ScrollView. In the screenshot you see the GridView (reddish) and it's parent VStack (greenish) have shrunk. The items inside the grid are still visible, rendered outside its area, but things do not scroll properly.
Why is the GridView not the size required to contain its items? If it did, I think this UI would scroll properly.
struct ContentView: View {
var body: some View {
ScrollView {
VStack {
Text("Section 1")
GridView(columns: 2, items: [1, 3, 5, 7, 9, 11, 13, 15]) { num in
Text("Item \(num)")
}
.background(Color.red.opacity(0.2))
}.background(Color.green.opacity(0.2))
}.background(Color.blue.opacity(0.2))
}
}
struct GridView<Content>: View where Content: View {
var columns: Int
let items: [Int]
let content: (Int) -> Content
init(columns: Int, items: [Int], #ViewBuilder content: #escaping (Int) -> Content) {
self.columns = columns
self.items = items
self.content = content
}
var rowCount: Int {
let (q, r) = items.count.quotientAndRemainder(dividingBy: columns)
return q + (r == 0 ? 0 : 1)
}
func elementFor(_ r: Int, _ c: Int) -> Int? {
let i = r * columns + c
if i >= items.count { return nil }
return items[i]
}
var body: some View {
GeometryReader { geo in
VStack {
ForEach(0..<self.rowCount) { ri in
HStack {
ForEach(0..<self.columns) { ci in
Group {
if self.elementFor(ri, ci) != nil {
self.content(self.elementFor(ri, ci)!)
.frame(width: geo.size.width / CGFloat(self.columns),
height: geo.size.width / CGFloat(self.columns))
} else {
Text("")
}
}
}
}
}
}
}
}
}
GeometryReader is a container view that defines its content as a function of its own size and coordinate space. Returns a flexible preferred size to its
parent layout.
in
struct ContentView: View {
var body: some View {
ScrollView {
VStack {
Text("Section 1")
GridView(columns: 2, items: [1, 3, 5, 7, 9, 11, 13, 15]) { num in
Text("Item \(num)")
}
.background(Color.red.opacity(0.5))
}.background(Color.green.opacity(0.2))
}.background(Color.blue.opacity(0.2))
}
}
What is the height of GridView?
Hm ... number of lines multiplied by height of grid cell, which is height of GridView divided by number of lines ...
The equation has no solution :-)
Yes I can see, you defined height of grid cell as width of GridView divided by number of columns, but SwiftUI is not so clever.
You have to calculate the height of the GridView. I fixed number of rows to 4 to simplify it ...
struct ContentView: View {
var body: some View {
GeometryReader { g in
ScrollView {
VStack {
Text("Section 1")
GridView(columns: 2, items: [1, 3, 5, 7, 9, 11, 13, 15]) { num in
Text("Item \(num)")
}.frame(height: g.size.width / 2 * CGFloat(4))
.background(Color.red.opacity(0.5))
}.background(Color.green.opacity(0.2))
}.background(Color.blue.opacity(0.2))
}
}
}
You better remove the geometry reader and grid cell frame from GridView.body and set the size of cell in your content view.
struct ContentView: View {
var body: some View {
GeometryReader { g in
ScrollView {
VStack {
Text("Section 1")
GridView(columns: 2, items: [1, 3, 5, 7, 9, 11, 13, 15]) { num in
Text("Item \(num)").frame(width: g.size.width / 2, height: g.size .width / 2)
}
.background(Color.red.opacity(0.5))
}.background(Color.green.opacity(0.2))
}.background(Color.blue.opacity(0.2))
}
}
}
As you can see, there is no need to define height of GridView, the equation has solution now.

Control z-index in Fabric.js

In fabricjs, I want to create a scene in which the object under the mouse rises to the top of the scene in z-index, then once the mouse leaves that object, it goes back to the z-index where it came from. One cannot set object.zindex (which would be nice). Instead, I'm using a placeholder object which is put into the object list at the old position, and then the old object is put back in the position where it was in the list using canvas.insertAt. However this is not working.
See http://jsfiddle.net/rFSEV/ for the status of this.
var canvasS = new fabric.Canvas('canvasS', { renderOnAddition: false, hoverCursor: 'pointer', selection: false });
var bars = {}; //storage for bars (bar number indexed by group object)
var selectedBar = null; //selected bar (group object)
var placeholder = new fabric.Text("XXXXX", { fontSize: 12 });
//pass null or a bar
function selectBar(bar) {
if (selectedBar) {
//remove the old topmost bar and put it back in the right zindex
//PROBLEM: It doesn't go back; it stays at the same zindex
selectedBar.remove();
canvasS.insertAt(selectedBar, selectedBar.XZIndex, true);
selectedBar = null;
}
if (bar) {
//put a placeholder object ("XXX" for now) in the position
//where the bar was, and put the bar in the top position
//so it shows topmost
selectedBar = bar;
canvasS.insertAt(placeholder, selectedBar.XZIndex, true);
canvasS.add(bar);
canvasS.renderAll();
}
}
canvasS.on({
'mouse:move': function(e) {
//hook up dynamic zorder
if (!e.target) return;
if (bars[e.target])
selectBar(e.target);
else
selectBar(null);
},
});
var objcount = canvasS.getObjects().length;
//create bars
for (var i = 0; i < 20; ++i) {
var rect = new fabric.Rect({
left: 0,
top: 0,
rx: 3,
ry: 3,
stroke: 'red',
width: 200,
height: 25
});
rect.setGradientFill({
x1: 0,
y1: 0,
x2: 0,
y2: rect.height,
colorStops: {
0: '#080',
1: '#fff'
}
});
var text = new fabric.Text("Bar number " + (i+1), {
fontSize: 12
});
var group = new fabric.Group([ rect, text ], {
left: i + 101,
top: i * 4 + 26
});
group.hasControls = group.hasBorders = false;
//our properties (not part of fabric)
group.XBar = rect;
group.XZIndex = objcount++;
canvasS.add(group);
bars[group] = i;
}
canvasS.renderAll();
Since fabric.js version 1.1.4 a new method for zIndex manipulation is available:
canvas.moveTo(object, index);
object.moveTo(index);
I think this is helpful for your use case. I've updated your jsfiddle - i hope this is what you want:
jsfiddle
Also make sure you change z-index AFTER adding object to canvas.
So code will looks like:
canvas.add(object);
canvas.moveTo(object, index);
Otherwise fabricjs don`t care about z-indexes you setup.
After I added a line object, I was make the line appear under the object using:
canvas.add(line);
canvas.sendToBack(line);
Other options are
canvas.sendBackwards
canvas.sendToBack
canvas.bringForward
canvas.bringToFront
see: https://github.com/fabricjs/fabric.js/issues/135
You can modify your _chooseObjectsToRender method to have the following change at the end of it, and you'll be able to achieve css-style zIndexing.
objsToRender = objsToRender.sort(function(a, b) {
var sortValue = 0, az = a.zIndex || 0, bz = b.zIndex || 0;
if (az < bz) {
sortValue = -1;
}
else if (az > bz) {
sortValue = 1;
}
return sortValue;
});
https://github.com/fabricjs/fabric.js/pull/5088/files
You can use these two functions to get z-index of a fabric object and modify an object's z-index, since there is not specific method to modify z-index by object index :
fabric.Object.prototype.getZIndex = function() {
return this.canvas.getObjects().indexOf(this);
}
fabric.Canvas.prototype.moveToLayer = function(object,position) {
while(object.getZIndex() > position) {
this.sendBackwards(object);
}
}

Resources