SwiftUI handling SpriteKit scene recreation when data changes - core-data

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

Related

How avoid that when keyboard appear move the layout up in SwiftUI? [duplicate]

I am using TextField in my view and when it becomes the first responder, it lefts the view as shown in the below GIF.
Is there any way I can get rid of this behavior?
Here is my code
NavigationView(content: {
ZStack{
MyTabView(selectedIndex: self.$index)
.view(item: self.item1) {
NewView(title: "Hello1").navigationBarTitle("")
.navigationBarHidden(true)
}
.view(item: self.item2) {
NewView(title: "Hello2").navigationBarTitle("")
.navigationBarHidden(true)
}
.view(item: self.item3) {
NewView(title: "Hello3").navigationBarTitle("")
.navigationBarHidden(true)
}
}.navigationBarHidden(true)
.navigationBarTitle("")
}).ignoresSafeArea(.keyboard, edges: .bottom)
// New View
struct NewView:View {
#State var text:String = ""
var title:String
var body: some View {
VStack {
Spacer()
Text("Hello")
TextField(title, text: self.$text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}.padding()
.onAppear {
debugPrint("OnApper \(self.title)")
}
}
}
For .ignoresSafeArea to work you need to fill all the available area (eg. by using a Spacer).
The following will not work (no Spacers, just a TextField):
struct ContentView: View {
#State var text: String = ""
var body: some View {
VStack {
TextField("asd", text: self.$text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}
However, it will work when you add Spacers (fill all the available space):
struct ContentView: View {
#State var text: String = ""
var body: some View {
VStack {
Spacer()
TextField("asd", text: self.$text)
.textFieldStyle(RoundedBorderTextFieldStyle())
Spacer()
}
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}
If you don't want to use Spacers you can also use a GeometryReader:
struct ContentView: View {
#State var text: String = ""
var body: some View {
GeometryReader { _ in
...
}
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}
You should apply the modifier on the ZStack, NOT the NavigationView
NavigationView(content: {
ZStack{
,,,
}.navigationBarHidden(true)
.navigationBarTitle("")
.ignoresSafeArea(.keyboard, edges: .bottom) // <- This line moved up
})
Full working example:
struct ContentView: View {
#State var text = ""
var body: some View {
VStack{
Spacer()
Text("Hello, World")
TextField("Tap to test keyboard ignoring", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
.padding()
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}
What eventually worked for me, combining answers posted here and considering also this question, is the following (Xcode 12.4, iOS 14.4):
GeometryReader { _ in
VStack {
Spacer()
TextField("Type something...", text: $value)
Spacer()
}.ignoresSafeArea(.keyboard, edges: .bottom)
}
Both spacers are there to center vertically the textfield.
Using only the GeometryReader or the ignoresSafeArea modifier didn't do the trick, but after putting them all together as shown above stopped eventually the view from moving up upon keyboard appearance.
That's what I figured out:
GeometryReader { _ in
ZStack {
//PUT CONTENT HERE
}.frame(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
}
It seems to work for me. In this case you do not need to check iOS 14 availability.

OnAppear calls unexpectedly when Keyboard Appears in SwiftUI

I am experiencing very odd behavior in SwiftUI 2.0 and iOS14.
When the keyboard appears on the screen, the OnAppear method of other tab's view called automatically.
However, this works fine Xcode 11.7
Here is the issue in action.
Here is the code which produces the above error.
struct ContentView: View {
var body: some View {
TabView {
DemoView(screenName: "Home")
.tabItem {
Image.init(systemName: "star.fill")
Text("Home")
}
DemoView(screenName: "Result")
.tabItem {
Image.init(systemName: "star.fill")
Text("Result")
}
DemoView(screenName: "More")
.tabItem {
Image.init(systemName: "star.fill")
Text("More")
}
}
}
}
struct DemoView:View {
#State var text:String = ""
var screenName:String
var body: some View {
VStack{
Text(screenName)
.font(.title)
TextField("Buggy Keyboard Issue", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
Text("Issue : When keyboard appears, onAppear of other 2 tabs call automatically.")
.font(.footnote)
}
.padding()
.onAppear(perform: {
debugPrint("OnAppear of : \(screenName)")
})
}
}
This seems to be a bug of SwiftUI 2.0 but not sure.
Any help will be appreciated.
Thanks
Having the same issue myself, I think this is a bug or something like that, however I came up with a solution maybe a workaround until apple will fix it.
The thing that I did is basically I used a LazyVStack, and this seems to be working perfectly.
LazyVStack {
VStack{
Text(screenName)
.font(.title)
TextField("Buggy Keyboard Issue", text: $text)
.textFieldStyle(RoundedBorderTextFieldStyle())
Text("Issue : When keyboard appears, onAppear of other 2 tabs call automatically.")
.font(.footnote)
}
.padding()
.onAppear(perform: {
debugPrint("OnAppear of : \(screenName)")
})
}
Now the OnAppear method of other tab's view it is not called automatically when the keyboard appear.
Just implemented the following workaround:
struct ContentView: View {
var body: some View {
TabView(selection: $selectedTab) {
TabContentView(tag: 0, selectedTag: selectedTab) {
Text("Some tab content")
}
.tabItem {
Text("First tab")
}
TabContentView(tag: 0, selectedTag: selectedTab) {
Text("Another tab content")
}
.tabItem {
Text("Second tab")
}
}
}
#State private var selectedTab: Int = 0
}
private struct TabContentView<Content: View, Tag: Hashable>: View {
init(tag: Tag, selectedTag: Tag, #ViewBuilder content: #escaping () -> Content) {
self.tag = tag
self.selectedTag = selectedTag
self.content = content
}
var body: some View {
Group {
if tag == selectedTag {
content()
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
Color.clear
}
}
.tag(tag)
}
private let tag: Tag
private let selectedTag: Tag
private let content: () -> Content
}
Not sure if it's stable enough but keyboard appearance doesn't trigger onAppear on tabs content anymore.
To avoid reloading your view try with on the TabView
.ignoresSafeArea(.keyboard, edges: .bottom)
It only works on iOS 14

SwiftUI layout grows outside the bounds of the device when using .edgesIgnoringSafeArea()

Having an issue in SwiftUI where some Views are growing bigger vertically than the size of the device when using .edgesIgnoringSafeArea(.bottom). On an iPhone 11 Pro which is 812 pixels high I am seeing a view of size 846. I am using the Debug View Hierarchy to verify it. This has been tested on Xcode 11.4.1 and 11.1 and exists in both versions and probably all in between.
I have included sample code below.
I am pretty sure this is a SwiftUI bug, but was wondering if anyone has a workaround for it. I need the edgesIgnoringSafeArea(.bottom) code to draw the TabBar, and for the ProfileView() to extend to the bottom of the screen when I hide my custom tab bar.
struct ContentView: View {
var body: some View {
MainTabView()
}
}
struct MainTabView : View {
enum Item : CaseIterable {
case home
case resources
case profile
}
#State private var selected : Item = .home
var body: some View {
VStack(spacing: 0.0) {
ZStack {
HomeView()
.zIndex(selected == .home ? 1 : 0)
ResourcesView()
.zIndex(selected == .resources ? 1 : 0)
ProfileView()
.zIndex(selected == .profile ? 1 : 0)
}
// Code here for building and showing/hiding a Toolbar
// Basically just a HStack with a few buttons in it
}
.edgesIgnoringSafeArea(.bottom) // <- This causes the screen to jump to 846
}
}
struct ProfileView : View {
#State private var showQuestionnaireView = false
var body: some View {
NavigationView {
ZStack {
NavigationLink(destination: QuestionnaireView( showQuestionnaireView:$showQuestionnaireView),
isActive: $showQuestionnaireView) {
Text("Show Questionnaire View")
}
.navigationBarTitle("")
.navigationBarHidden(true)
}
}
}
}
struct QuestionnaireView : View {
#Binding var showQuestionnaireView : Bool
var body: some View {
GeometryReader { screenGeometry in
ZStack {
Color.orange
VStack {
Text("Top")
Spacer()
Text("Bottom")
}
}
}
}
}
HomeView() and ResourcesView() are just copies of ProfileView() that do their own thing.
When you run it you will see a button, push the button and a hidden Navigation Stack View pushes on the QuestionnaireView, this view contains a VStack with two text fields, neither of which you will be able to see due to this issue. Understandably the top one is behind the notch, but the bottom one is off the bottom of the screen. In my real project this issue is rarely seen at runtime, but switching between dark mode and light mode shows it. In the above code there is no need to switch appearances.
EDIT: FB7677794 for anyone interested, have not received any updates from Apple since lodging it 3 weeks ago.
EDIT2: Added some more code to MainTabBar
Update: This is fixed in Xcode 12 Beta 2
After reading the updated question I have made some changes and tried to make a small demo. In this, I am using the same approach as before, put NavigationView in your main tab view and with this you don't have to hide and show every time you come or leave your main tab view.
import SwiftUI
struct ContentView: View {
var body: some View {
MainTabView()
}
}
struct MainTabView : View {
enum Item : CaseIterable {
case home
case resources
case profile
}
#State private var selected : Item = .home
var body: some View {
NavigationView {
VStack(spacing: 0.0) {
ZStack {
Group {
HomeView()
.zIndex(selected == .home ? 1 : 0)
ResourcesView()
.zIndex(selected == .resources ? 1 : 0)
ProfileView()
.zIndex(selected == .profile ? 1 : 0)
}
.frame(minWidth: .zero, maxWidth: .infinity, minHeight: .zero, maxHeight: .infinity)
.background(Color.white)
}
HStack {
Group {
Image(systemName: "house.fill")
.onTapGesture {
self.selected = .home
}
Spacer()
Image(systemName: "plus.app.fill")
.onTapGesture {
self.selected = .resources
}
Spacer()
Image(systemName: "questionmark.square.fill")
.onTapGesture {
self.selected = .profile
}
}
.padding(.horizontal, 30)
}
.frame(height: 40)
.foregroundColor(Color.white)
.background(Color.gray)
// Code here for building and showing/hiding a Toolbar
// Basically just a HStack with a few buttons in it
}
.edgesIgnoringSafeArea(.bottom)
} // <- This causes the screen to jump to 846
}
}
struct ProfileView : View {
#State private var showQuestionnaireView = false
var body: some View {
// NavigationView {
ZStack {
NavigationLink(destination: QuestionnaireView( showQuestionnaireView:$showQuestionnaireView),
isActive: $showQuestionnaireView) {
Text("Show Questionnaire View")
}
.navigationBarTitle("")
.navigationBarHidden(true)
}
// }
}
}
struct QuestionnaireView : View {
#Binding var showQuestionnaireView : Bool
var body: some View {
GeometryReader { screenGeometry in
ZStack {
Color.orange
VStack {
Text("Top")
Spacer()
Text("Bottom")
}
}
.edgesIgnoringSafeArea(.bottom)
}
}
}
struct HomeView: View {
var body: some View {
NavigationLink(destination: SecondView()) {
Text("Home View")
}
}
}
struct ResourcesView: View {
var body: some View {
NavigationLink(destination: SecondView()) {
Text("Resources View")
}
}
}
struct SecondView: View {
var body: some View {
Text("Second view in navigation")
.background(Color.black)
.foregroundColor(.white)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewDevice(PreviewDevice(rawValue: "iPhone 11"))
}
}
It is due to undefined size for NavigationView. When you add your custom tab bar component, as in example below, that limits bottom area, the NavigationView will layout correctly.
Tested with Xcode 11.4 / iOS 13.4
struct MainTabView : View {
var body: some View {
VStack(spacing: 0.0) {
ZStack {
Color(.cyan)
ProfileView() // << this injects NavigationView
}
HStack { // custom tab bar
Button(action: {}) { Image(systemName: "1.circle").padding() }
Button(action: {}) { Image(systemName: "2.circle").padding() }
Button(action: {}) { Image(systemName: "3.circle").padding() }
}.padding(.bottom)
}
.edgesIgnoringSafeArea(.bottom) // works !!
}
}

edit a Core Data Object in SwiftUI

I push an object that comes from the Core Data Database to a detail page with text fields.
When the user changes the text in the textfields and presses save the changes should be saved to the core data DB.
Problem: I have no clue how to modify/update/change an existing core data entity.
I probably need to get the original via a #FetchRequest but every time I try I get some problems.
Question 1: Let's say the entity has object.id as UUID, how can I
fetch that object in SwiftUI?
Question 2: How can I overwrite the fetched object with the changed
content of the textfields?
struct ProductDetail: View {
#State var barcode: Barcode
#Environment(\.presentationMode) var presentationMode
#Environment(\.managedObjectContext) var context
//let datahandler = Datahandler()
var body: some View {
VStack {
HStack {
Text("Barcode: ")
Spacer()
TextField("Barcode", text: Binding($barcode.code, ""))
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 250, alignment: .trailing)
}
HStack {
Text("Amount: ")
Spacer()
TextField("Amount", text: Binding($barcode.amount, ""))
.textFieldStyle(RoundedBorderTextFieldStyle())
.frame(width: 250, alignment: .trailing)
.keyboardType(.numberPad)
}
HStack{
Button("Back") {
self.presentationMode.wrappedValue.dismiss()
}
Spacer()
Button("Save"){
//self.datahandler.updateBarcode(barcode: self.object)
self.editBarcode(barcode: barcode)
self.presentationMode.wrappedValue.dismiss()
}
}
.padding()
}
.padding()
}
func editBarcode(barcode: Barcode) {
// Question 1: Fetch Original object using barcode.id
// Question 2: How to but barcode into context so it can overwrite the core data original?
try? context.save()
}
Attempt:
func editBarcode(barcode: Barcode) {
#FetchRequest(sortDescriptors: [], predicate: NSPredicate(format: "self.id IN %#", barcode.id)) var results: FetchedResults<Barcode>
results.first?.amount = barcode.amount
results.first?.code = barcode.code
try? context.save()
}
Errors:
Argument type 'UUID?' does not conform to expected type 'CVarArg'
Cannot use instance member 'barcode' within property initializer;
property initializers run before 'self' is available
Property wrappers are not yet supported on local properties

How to show my AVPlayer in a VStack with SwiftUI

I have created a simple AVPlayer. (If I didn't create it correctly, please help me fixing it... 😀 ) I want to display it in my VStack with swift ui, but I'm kind of stuck...
If at least there was the AVPlayer View component in the library, it would have been easier, but I didn't found it in the library... 😔
Here is my code in the ContentView.swift:
//
// ContentView.swift
// test
//
// Created by Francis Dolbec on 2019-06-26.
// Copyright © 2019 Francis Dolbec. All rights reserved.
//
import SwiftUI
import AVKit
import AVFoundation
// MARK: variables
var hauteurMenuBar = NSApplication.shared.mainMenu?.menuBarHeight
var urlVideo = URL(string: "/Users/francisdolbec/Movies/Séries Télé/Rick et Morty/Rick.and.Morty.S01E01.VFQ.HDTV.1080p.x264-Kamek.mp4")
let player = AVPlayer(url: urlVideo!)
struct ContentView : View {
var body: some View {
VStack {
Text("Hello World")
.frame(maxWidth: .infinity, maxHeight: .infinity)
Text("PATATE!")
//player.play()
}
.frame(minWidth: 1024, idealWidth: 1440, maxWidth: .infinity, minHeight: (640-hauteurMenuBar!), idealHeight: (900-hauteurMenuBar!), maxHeight: .infinity)
}
}
#if DEBUG
struct ContentView_Previews : PreviewProvider {
static var previews: some View {
ContentView()
}
}
#endif
By the way, if there's a tutorial that show how to make a video player with swift ui, I would really appreciate it!
EDIT
I forgot to say that I am developing a macOS app.
Sure, here's the tutorial, by Chris Mash on Medium.
Basically you embed an AVPlayerLayer into a struct conforming to UIViewRepresentable, something you'll do for any UIView component you want to use with SwiftUI.
struct PlayerView: UIViewRepresentable {
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<PlayerView>) {
}
func makeUIView(context: Context) -> UIView {
return PlayerUIView(frame: .zero)
}
}
The "meat" of the implementation is done in the PlayerUIView class:
class PlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
override init(frame: CGRect) {
super.init(frame: frame)
let url = URL(string: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8")!
let player = AVPlayer(url: url)
player.play()
playerLayer.player = player
layer.addSublayer(playerLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
}
And then you use it like this:
var body: some View {
PlayerView()
}
Thanks to Bogdan for the quick answer and for the like to the tutorial!
Here is the code, converted to NSView so it could work for macOS apps...
Here is the struct PlayerView :
struct PlayerView: NSViewRepresentable {
func updateNSView(_ nsView: NSView, context: NSViewRepresentableContext<PlayerView>) {
}
func makeNSView(context: Context) -> NSView {
return PlayerNSView(frame: .zero)
}
}
Here is the class, "the meat" like Bogdan said:
class PlayerNSView: NSView{
private let playerLayer = AVPlayerLayer()
override init(frame:CGRect){
super.init(frame: frame)
let urlVideo = URL(string: "https://bitdash-a.akamaihd.net/content/sintel/hls/playlist.m3u8")!
let player = AVPlayer(url: urlVideo)
player.play()
playerLayer.player = player
if layer == nil{
layer = CALayer()
}
layer?.addSublayer(playerLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layout() {
super.layout()
playerLayer.frame = bounds
}
}
Finally you can use PlayerView() to add the AVPlayer in the struct ContentView.

Resources