Determine actual height of a SpriteKit SKScene inside a SwiftUI View - layout

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)
}
}
}

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

Make TextEditor dynamic height SwiftUI

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!

SwiftUI: height and spacing of Views in GeometryReader within a ScrollView

ScrollView {
VStack() {
ForEach(0 ..< 5) { item in
Text("Hello There")
.font(.largeTitle)
}
}
}
The above code results in this:
When we add a GeometryReader, the views lose their sizing and spacing from one another (the .position modifier is just to center the text views within the GeometryReader):
ScrollView {
VStack() {
ForEach(0 ..< 5) { item in
GeometryReader { geometry in
Text("Hello There")
.font(.largeTitle)
.position(x: UIScreen.main.bounds.size.width/2)
}
}
}
}
Can anybody share some help/advice around why this is the behavior and what I may want to look into in order to use a GeometryReader on center aligned views within a ScrollView as I'm trying to do here?
Adding a .frame(minHeight: 40) modifier to the ForEach is one way to force the views to take the space that the Text needs, but that is not a very nice or scalable solution.
Thank you!
If you need just width of screen then place GeometryReader outside of scrollview:
GeometryReader { geometry in // << here !!
ScrollView {
VStack() {
ForEach(0 ..< 5) { item in
Text("Hello There")
.font(.largeTitle)
.position(x: UIScreen.main.bounds.size.width/2)
}
}
}
}
Can anybody share some help/advice around why this is the behavior
ScrollView does not have own size so GeometryReader cannot read anything from it and provides for own children some minimal dimension.

SwiftUI handling SpriteKit scene recreation when data changes

I have a SwiftUI view that is connected to a CoreData model. I also have a SpriteKit scene that changes data in my model. So every time I manipulate my data from my SKScene in CoreData my scene gets reinitialised which is an unwanted behaviour in my case.
How can I get the updated model in my SpriteView without the SKScene being recreated?
My code looks like this:
struct TamagotchiListView: View {
#Environment(\.managedObjectContext)
var context: NSManagedObjectContext
#FetchRequest(fetchRequest: TamagotchiModel.getFetchRequest())
var tamagotchis: FetchedResults<TamagotchiModel>
var body: some View {
VStack {
List {
ForEach(tamagotchis, id: \.self) { (tamagotchi: TamagotchiModel) in
NavigationLink(destination: SpriteKitView(scene: SpriteKitScene(model: tamagotchi))) {
HStack {
Image(systemName: "gamecontroller")
.padding(.trailing, 5)
VStack(alignment: .leading) {
Text(tamagotchi.name)
.font(.headline)
Spacer()
Text(tamagotchi.birthDate, style: .date)
}
Spacer()
}
}
}
}
}
}
I managed to work around my problem by creating a view model that manages the SpriteKit scene creation if needed.
class TamagotchiViewModel {
private var spriteKitScenes: [SpriteKitScene] = []
func scene(for tamagotchi: TamagotchiModel) -> SpriteKitScene {
if let scene = spriteKitScenes.first(where: { $0.tamagotchi?.tamagotchiModel.id == tamagotchi.id}) {
return scene
} else {
let newScene = SpriteKitScene(model: tamagotchi)
spriteKitScenes.append(newScene)
return newScene
}
}
}

How to stop SwiftUI view from getting wider than superview

I created a UIHostingController to put a SwiftUI view in my app. The root view contains a VStack, and inside that some HStacks and Pickers. The picker pushes the view wider than the screen by 8 points. It looks bad - text is slightly off screen. Usually I would fix this with auto layout constraints that pin the left and right edges of the view to the superview, with priority 1000 (required). What is the equivalent in SwiftUI?
Some boiled down code that triggers the problem
struct EditSegmentView: View {
#State var seconds: Int = 10
var body: some View {
VStack {
HStack {
Text("Pause after intro")
Spacer()
Text("10 seconds")
}
Picker(selection: $seconds, label: Text("Seconds")) {
ForEach(0..<60) { n in
Text("\(n) sec").tag(n)
}
}
}
}
}
That is displayed from a UIViewController, like this:
let editView = EditSegmentView()
let vc = UIHostingController(rootView: editView)
vc.modalPresentationStyle = .fullScreen
present(vc, animated: true)
I can't see anything obviously wrong with the code provided. You could try using a GeometryReader to force the VStack to be the width of the available space. Also add some padding(), because otherwise your Text will start right at the edge
var body: some View {
GeometryReader { proxy in
VStack {
//...
} .padding()
.frame(width: proxy.size.width)
}
}
Are there any clues as to why the view is expanding beyond the edges when you debug its view hierarchy?
This is a little late, but I had the same issue and fixed by making sure the outer VStack had it's alignment set to .leading. The inner Vstack also has alignment: leading and then added a .padding() to the inner VStack

Resources