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
Related
I just started with SwiftUI and have basic questions.
I try to show the value of the rotation in the Text field. The error message I get is: "No exact matches in call to initializer"
Where is the mistake?
import SwiftUI
struct ContentView: View {
#State var rotation: Double = 0
var body: some View {
VStack {
VStack {
Text("Hello, world!")
.padding()
Slider(value: $rotation, in: 0 ... 360, step: 0.1)
.padding()
Text(rotation)
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
and for fun you can also try this:
struct ContentView: View {
#State var rotation: Double = 0
var body: some View {
VStack {
Text("Hello, world!").rotationEffect(Angle(degrees: rotation)).padding()
Slider(value: $rotation, in: 0 ... 360, step: 0.1).padding()
Text("\(rotation)")
}
}
}
You should return a String type for initialization of Text, like this:
struct ContentView: View {
#State var rotation: Double = 0
var body: some View {
VStack {
VStack {
Text("Hello, world!")
.padding()
Slider(value: $rotation, in: 0 ... 360, step: 0.1)
.padding()
Text(rotation.string)
}
}
}
}
extension CustomStringConvertible { var string: String { get { return String(describing: self) } } }
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
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!
I have a SwiftUI View with a VStack of 3 SpriteKit SKScenes inside.
While scoreView and accelerometerView have a fixed height, gameScene does not.
It takes the remaining space and depends somehow on the device.
struct ContentView: View {
#StateObject private var gameScene = GameSKScene()
#StateObject private var scoreView = ScoreSKScene()
#StateObject private var accelerometerView = AccelerometerSKScene()
var body: some View {
VStack {
SpriteView(scene: scoreView)
.frame(height: 50)
SpriteView(scene: gameScene)
SpriteView(scene: accelerometerView)
.frame(height: 50)
}.ignoresSafeArea()
}
}
Now inside gameScene have problems setting up the scene because the scene itself has no information of its size.
I can get the width with UIScreen.main.bounds.width but how do I get the actual height?
If I understand you correctly, you want the gameScene SpriteView to fill the remaining space after accounting for the other two scenes. You can use GeometryReader (documentation here).
Example usage:
var body: some View {
GeometryReader { geo in
VStack {
SpriteView(scene: scoreView)
.frame(height: 50)
SpriteView(scene: gameScene)
.frame(height: geo.size.height - 100)
SpriteView(scene: accelerometerView)
.frame(height: 50)
}
}
}
I have following code. If I change .background(Color.green) to .background(Color.white) for the VStack the background will be the systemGray I used for the Listbackground.
Has it something to do with the .colorMultiply(Color(UIColor.systemGray4)) property?.
var body: some View {
NavigationView {
List {
Text("Bla bla bla")
Group {
VStack {
TextField("Server address", text: $serverAddress)
.keyboardType(.default)
TextField("Server port", text: $serverPortString)
.keyboardType(.numberPad)
}
}
.padding()
.background(Color.green)
.cornerRadius(8)
// Some more elements
}
.navigationBarHidden(false)
.navigationBarTitle("Connect your Server", displayMode: .large)
}
.colorMultiply(Color(UIColor.systemGray4))
.onTapGesture {
self.hideKeyboard()
}
White background that get ignored:
Working green background:
.colorMultiply() adds a color multiplication effect to the view that it is applied on, meaning that it tints it the color that you have added. As your NavigationView is the containing view, all views inside it will be tinted systemGray4.
When you set it to white, the systemGray4 tints that so that it blends in with the background as that is also white.
You could do something like this:
struct ContentView: View {
#State var serverAddress: String = ""
#State var serverPortString: String = ""
let backgroundColor = Color.init(UIColor.systemGray4)
init() {
UITableView.appearance().backgroundColor = .clear // or you could set this to systemGray4 and ignore the ZStack
UITableView.appearance().separatorColor = .clear
UITableView.appearance().tableFooterView = UIView()
UITableView.appearance().separatorStyle = .none
}
var body: some View {
NavigationView {
ZStack {
backgroundColor.frame(maxWidth: .infinity, maxHeight: .infinity).edgesIgnoringSafeArea(.all)
List {
Text("Bla bla bla")
.listRowBackground(backgroundColor)
Group {
VStack {
TextField("Server address", text: $serverAddress)
.keyboardType(.default)
TextField("Server port", text: $serverPortString)
.keyboardType(.numberPad)
}
}
.listRowBackground(backgroundColor)
.padding()
.background(Color.white)
.cornerRadius(8)
// Some more elements
}
}
.navigationBarHidden(false)
.navigationBarTitle("Connect your Server", displayMode: .large)
}
.onTapGesture {
print("tapped")
}
}
}
Which will give you this result: