I have a #State property that's initialized with a struct I created (Landmark) that takes in a MKPlacemark.
#State private var selectedLandmark: Landmark = Landmark(placemark: MKPlacemark())
Here's the code for the struct:
import Foundation
import MapKit
struct Landmark {
let placemark: MKPlacemark
var id: UUID {
UUID()
}
var name: String {
placemark.name ?? ""
}
var title: String {
placemark.title ?? ""
}
var coordinate: CLLocationCoordinate2D {
placemark.coordinate
}
}
Whenever there are any updates to any state/binding vars within this view, I get a EXC_BAD_ACCESS error and no logs. If I remove the reference to MKPlacemark, everything works fine. Is this possibly a case of a strong reference being retained somewhere?
Steps to reproduce:
Create a "child" view:
import SwiftUI
import MapKit
struct MKPlaceChild: View {
#Binding var showCreateEvent: Bool
#State private var selectedLandmark: Landmark = Landmark(placemark: MKPlacemark())
var body: some View {
VStack {
Text("Hello, World!")
Button("Cancel") {
self.showCreateEvent = false
}
}
}
}
Create a parent view:
struct MKPlaceParent: View {
#State var showCreateEvent: Bool = true
var body: some View {
VStack {
if showCreateEvent {
MKPlaceChild(showCreateEvent: $showCreateEvent)
} else {
Text("Hello")
}
}
}
}
If you tap on "Cancel" button, an error occurs :(
Any help would be appreciated
The problem is in wrong initialiser. Here is fixed part (tested with Xcode 12 / iOS 14)
struct MKPlaceChild: View {
#Binding var showCreateEvent: Bool
#State private var selectedLandmark: Landmark = Landmark(placemark:
MKPlacemark(coordinate: CLLocationCoordinate2D())) // << here !!
...
Related
The background color and text in ReadyDashboardView below don't update when isConnected is updated. Obviously I want to update the view when the connection is made. I was expecting that publishing everything in the chain to that variable would make it update instantly in swiftui. Instead, it's always rendering using the value provided when the variable is instantiated. Here's a very simplified look at the situation:
Is there another swiftui feature i should be using or am I going to have to make some sweeping changes to my codebase?
import SwiftUI
#main
struct TestApp: App {
#StateObject private var env = PrinterEnv()
var body: some Scene {
WindowGroup {
ReadyDashboardView()
.environmentObject(env)
}
}
}
struct ReadyDashboardView: View {
#EnvironmentObject var env: PrinterEnv
var body: some View {
VStack {
HStack {
Spacer()
VStack {
Text(env.selectedPrinter?.isConnected ?? false ? "Printer Ready" : "Not Connected")
.padding(.bottom)
}
Spacer()
}
.background(env.selectedPrinter?.isConnected ?? false ? .green : .red)
Button("Connect") { env.selectedPrinter?.isConnected = true }
Button("Disconnect") { env.selectedPrinter?.isConnected = false }
}
}
}
class PrinterEnv: ObservableObject {
#Published var configuredPrinters: [Printer] = []
#Published var selectedPrinter: Printer?
init() {
configuredPrinters.append(contentsOf: [Printer()])
selectedPrinter = configuredPrinters.first
}
}
class Printer: ObservableObject {
#Published var isConnected = false
}
I suggest you do not nest ObservableObject, it does not work very well.
Try a Printer struct for example, such as:
struct Printer: Identifiable {
let id = UUID()
var isConnected = false
}
I do not think the problem is specifically related to nested observable objects. Nesting them is working fine and is even recommended by community members as the best way to manage app wide states and ensure performance is acceptable.
See this: https://www.fivestars.blog/articles/app-state/
That said, I believe it's probably related to the wrapping of the observable object with #Published property wrapper.
Try this in a playground:
import SwiftUI
class AppState: ObservableObject {
let fooState = FooState()
let barState = BarState()
}
class FooState: ObservableObject {
#Published var foo: Int = 42
}
class BarState: ObservableObject {
#Published var bar: String = Date().debugDescription
}
struct FooView: View {
#EnvironmentObject var fooState: FooState
var body: some View {
Text("foo: \(fooState.foo)")
}
}
struct BarView: View {
#EnvironmentObject var barState: BarState
var body: some View {
Text("bar: \(barState.bar)")
}
}
struct ContentView: View {
#EnvironmentObject var appState: AppState
var body: some View {
VStack {
Spacer()
FooView()
.environmentObject(appState.fooState)
Button("update foo") {
appState.fooState.foo = Int.random(in: 1...100)
}
Spacer()
BarView()
.environmentObject(appState.barState)
Button("update bar") {
appState.barState.bar = Date().debugDescription
}
Spacer()
}
}
}
import PlaygroundSupport
PlaygroundPage.current.liveView = UIHostingController(
rootView: ContentView()
.frame(width: 320, height: 414)
.environmentObject(AppState())
)
In the process of making my first Finance App, I want the user to type their Credit Card Name and las four numbers (probably more info since this is a draft) into this Modally presented view, to then be seen in a cards index, widget-look-like.
struct CardListView: View {
#State var isPresentingAddModal = false
#State var emisorTarjeta = ""
#State var numeroTarjeta = ""
var headerView: some View {
HStack {
Text("Tus tarjetas")
Spacer()
Button("Añadir nueva") {
self.isPresentingAddModal.toggle()
}
.sheet(isPresented: $isPresentingAddModal, content: {
HStack {
Text("Emisor de tarjeta")
TextField("Seleccionar emisor de tarjeta", text: $emisorTarjeta)
}
HStack {
Text("Número de tarjeta")
TextField("Escribí tu número de tarjeta", text: $numeroTarjeta)
}
Button(action: {
self.isPresentingAddModal.toggle()
print("\(self.emisorTarjeta)")
}, label: {
Text("Añadir")
})
Spacer()
})
}
The question now is how to pass the info typed from the two textFields, to the view where the cards will be created. The button "Añadir" currently works as a dismiss button instead of an add one, since I don't know how to create that.
(Also, a lot of code like paddings and backgroundColors have been erased to make it clearer to see)
Enitre view of the homeView
Where the "añadir" button is
there are several ways to do this. One simple way is to use "#State" and "#Binding" like this:
In "CardListView" use this:
#Binding var emisorTarjeta: String
#Binding var numeroTarjeta: String
and in the "CardViewCreator" use:
#State var emisorTarjeta = ""
#State var numeroTarjeta = ""
Another way is to use "ObservableObject", create a class like this:
class CardModel: ObservableObject {
#Published var emisorTarjeta = ""
#Published var numeroTarjeta = ""
}
In the your "CardViewCreator" or some parent view:
#StateObject var cardModel = CardModel()
and pass it to the "CardListView" like this:
struct CardListView: View {
#ObservedObject var cardModel: CardModel
...
}
You can also use "EnvironmentObject" in a similar way.
It all depends on your case. I recommend reading up on "ObservedObject"
and using that.
A really simple way of doing this is to pass in a closure to run when the add button is tapped. Here's an example, which also shows how to dismiss the presented sheet
import SwiftUI
struct Card: Identifiable {
let id = UUID()
let provider: String
let number: String
}
struct ContentView: View {
#State private var cards = [Card]()
#State private var showingSheet = false
var body: some View {
VStack {
List(cards, rowContent: CardView.init)
.padding(.bottom, 10)
Button("Add") {
showingSheet = true
}
.padding()
}
.sheet(isPresented: $showingSheet) {
AddSheet(completion: addCard)
}
}
func addCard(provider: String, number: String) {
let newCard = Card(provider: provider, number: number)
cards.append(newCard)
}
}
struct CardView: View {
let card: Card
var body: some View {
VStack(alignment: .leading) {
Text(card.provider)
Text(card.number)
}
}
}
struct AddSheet: View {
#Environment(\.presentationMode) var presentationMode
#State private var provider = ""
#State private var number = ""
let completion: (String, String) -> Void
var body: some View {
VStack {
TextField("Provider", text: $provider).padding()
TextField("Number", text: $number).padding()
Button("Add") {
completion(provider, number)
presentationMode.wrappedValue.dismiss()
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
If you want to actually save the information passed in the textfield you would have to save it somewhere and later fetch it when required But this is only if you want to be able to access the information passed into the cards index after you have closed down the application and opened it up once again.
I have 4 views.
Grandparent
Parent
Child
EditView
Grandparent has a navigation link to Parent, and Parent a navigation link to Child. Child has a button which initializes a #State variable, location (a class), from Grandparent, via a binding in Parent and a binding in Child. That same button also updates a #State variable, showEditView, from Grandparent (via bindings again), which shows the EditView.
There are 11 lines which are currently commented out. If they are commented out, the app throws a "Fatal error: Unexpectedly found nil..." error when I tap the button in the Child view. If either section is uncommented, the button does not throw an error.
It also works if I pass the bound location to the EditView, not property itself like I'm currently doing, where it is then wrapped as an #ObservedObject.
I don't understand what is going on here. The only thing I can of is that, when it's working, SwiftUI is updating the location property because it's used in the body. If that is the case, that seems to indicate that I have to include a hidden Text view of this property every time I want to do have properties passed around this way.
Grandparent
import SwiftUI
struct Grandparent: View {
#State var location: Location!
#State var showEditView: Bool = false
var body: some View {
NavigationView {
VStack{
NavigationLink(
destination: Parent(location: $location, showEditView: $showEditView)) {
Text("Navigate")
}
// // section 1
// if location != nil {
// Text(location.name)
// } else {
// Text("No location yet")
// }
}
// // section 2
// .navigationBarItems(trailing:
// Button("Edit"){
// showEditView = true
// }
// .disabled(location == nil)
// )
}
.padding()
.sheet(isPresented: $showEditView) {
EditView(placemark: location, dismiss: { showEditView = false })
}
}
}
Parent
import SwiftUI
struct Parent: View {
#Binding var location: Location!
#Binding var showEditView: Bool
var body: some View {
NavigationLink(
destination: Child(location: $location, showEditView: $showEditView),
label: {
Text("Child")
})
}
}
Child
import SwiftUI
struct Child: View {
#Binding var location: Location!
#Binding var showEditView: Bool
var body: some View {
Button("Make location") {
location = Location(name: "Lebanon")
showEditView = true
}
}
}
EditView
import SwiftUI
struct EditView: View {
#ObservedObject var placemark: Location
var dismiss: () -> Void
var body: some View {
NavigationView {
Text(placemark.name)
.navigationTitle("Edit place")
.navigationBarItems(trailing: Button("Done") { dismiss() })
}
}
}
Location
import Foundation
class Location: ObservableObject {
init(name: String) {
self.name = name
}
#Published var name: String
}
You probably shouldn't declare your Location as ! and then do nil checking on it. Using ! is sort of asking for a crash to happen. I think what you're encountering is a the sheet getting rendered before location is set. There aren't any guarantees about when in the run loop a #State variable gets set, so it's better to account for scenarios where it is nil (and definitely not using ! to force unwrap it).
Secondly, at least given the scenario you have here, you probably shouldn't be using a class for Location -- it should just be a struct.
Eventually, you are going to run into a little bit of complexity, because judging by your View's name, you want to edit the Location at some point. This becomes a little more tricky with an Optional, since things like TextField want non-optional values, but this can be solved in various was (see where I used nonNilBinding).
Something like this is definitely a more safe approach than what you're currently doing. It may not be exactly what you want, but hopefully it can get you on the right path.
struct Location {
var name : String
}
struct Grandparent: View {
#State var location: Location?
#State var showEditView: Bool = false
var body: some View {
NavigationView {
VStack{
NavigationLink(
destination: Parent(location: $location, showEditView: $showEditView)) {
Text("Navigate")
}
if let location = location {
Text(location.name)
} else {
Text("No location yet")
}
}
.padding()
.sheet(isPresented: $showEditView) {
EditView(placemark: $location, dismiss: { showEditView = false })
}
}
}
}
struct Parent: View {
#Binding var location: Location?
#Binding var showEditView: Bool
var body: some View {
NavigationLink(
destination: Child(location: $location, showEditView: $showEditView),
label: {
Text("Child")
})
}
}
struct Child: View {
#Binding var location: Location?
#Binding var showEditView: Bool
var body: some View {
Button("Make location") {
location = Location(name: "Lebanon")
showEditView = true
}
}
}
struct EditView: View {
#Binding var placemark: Location?
var dismiss: () -> Void
var nonNilBinding : Binding<Location> {
.init { () -> Location in
placemark ?? Location(name:"Default")
} set: { (newValue) in
placemark = newValue
}
}
var body: some View {
NavigationView {
TextField("Name", text: nonNilBinding.name)
.navigationTitle("Edit place")
.navigationBarItems(trailing: Button("Done") { dismiss() })
}
}
}
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)
}
}
I will need to display a collapsed menu in SwiftUI, it is possible to pass one single bool value as binding var to subviews but got stuck when trying to pass that value from a dictionary.
see code below:
struct MenuView: View {
#EnvironmentObject var data: APIData
#State var menuCollapsed:[String: Bool] = [:]
#State var isMenuCollapsed = false;
// I am able to pass self.$isMenuCollapsed but self.$menuCollapsed[menuItem.name], why?
var body: some View {
if data.isMenuSynced {
List() {
ForEach((data.menuList?.content)!, id: \.name) { menuItem in
TopMenuRow(dataSource: menuItem, isCollapsed: self.$isMenuCollapsed)
.onTapGesture {
if menuItem.isExtendable() {
let isCollapsed = self.menuCollapsed[menuItem.name]
self.menuCollapsed.updateValue(!(isCollapsed ?? false), forKey: menuItem.name)
} else {
print("Go to link:\(menuItem.url)")
}
}
}
}
}else {
Text("Loading...")
}
}
}
in ChildMenu Row:
struct TopMenuRow: View {
var dataSource: MenuItemData
#Binding var isCollapsed: Bool
var body: some View {
ChildView(menuItemData)
if self.isCollapsed {
//display List of child data etc
}
}
}
}
If I use only one single bool as the binding var, the code is running ok, however, if I would like to use a dictionary to store each status of the array, it has the error of something else, see image blow:
if I use the line above, it's fine.
Any idea of how can I fix it?
Thanks
How to use dictionary as a storage of mutable values with State property wrapper?
As mentioned by Asperi, ForEach requires that source of data conforms to RandomAccessCollection. This requirements doesn't apply to State property wrapper!
Let see one of the possible approaches in the next snippet (copy - paste - run)
import SwiftUI
struct ContentView: View {
#State var dict = ["alfa":false, "beta":true, "gamma":false]
var body: some View {
List {
ForEach(Array(dict.keys), id: \.self) { (key) in
HStack {
Text(key)
Spacer()
Text(self.dict[key]?.description ?? "false").onTapGesture {
let v = self.dict[key] ?? false
self.dict[key] = !v
}.foregroundColor(self.dict[key] ?? false ? Color.red: Color.green)
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
with the following result