cI am using SwiftUI and custom annotations. Everything works ok if I pass an array of custom annotations MapCustomAnnotation directly from my View. However, when I try to fetch data from my local server the annotations are not shown even though the #Published array contains correct data. Here is the code:
struct MyMapView<ViewModel: MyViewModeling & ObservableObject>: View {
#ObservedObject private var viewModel: ViewModel
init(viewModel: ViewModel) {
self.viewModel = viewModel
}
var body: some View {
VStack {
MapView(region: getRegion(), annotation: viewModel.mapCustomAnnotations)
.onAppear(perform: viewModel.viewAppeared)
}
}
}
View Model
protocol MyViewModeling {
// other stuff
var mapCustomAnnotations: [MapCustomAnnotation] { get set }
}
final class MyViewModel: MyViewModeling & ObservableObject {
//other stuff
// MARK: - Published Properties
#Published var mapCustomAnnotations: [MapCustomAnnotation] = [MapCustomAnnotation]()
//other stuff to inject service etc etc
func fetchEvents() {
Task(priority: .utility) { // CHANGED IT AS SUGGESTED
let result = await self.service.getEventsForTypeWithMinAndMaxCoordinates(minCoordinates: CLLocationCoordinate2D(latitude: -2.323_24, longitude: 76.434_343_4), maxCoordinates: CLLocationCoordinate2D(latitude: -12.009_090, longitude: 75.434_535_3))
switch result {
case let .success(response):
await MainActor.run { // CHANGED IT AS SUGGESTED
//getMapPinViewModel simply initialize and append the MapCustomAnnotation objects from my model
mapCustomAnnotations = self.getMapPinViewModel(events: response.data)
}
case let .failure(error):
print(error.customMessage)
}
}
}
}
Has I said, setting a breakpoint inside my view, I can see that MapView is called again once the #Published array is updated but I cannot see the annotations> Everything works if I passed the following array directly in the view:
let data = [MapCustomAnnotation(name: "place1", coordinate: CLLocationCoordinate2D(latitude: 41.901_784, longitude: 12.426_366), type: .football),
MapCustomAnnotation(name: "place2", coordinate: CLLocationCoordinate2D(latitude: 41.900_784, longitude: 12.426_366), type: .volley)]
Do I need to trigger the view update somehow?
If this happens to anyone, you need to call the following to manually update the annotations:
func updateUIView(_ map: MKMapView, context _: Context) {
map.showAnnotations(mapCustomAnnotation, animated: true)
}
Related
I want to make it so you can favourite a "landmark" in one view (LandmarkDetail), and access a list of all the "landmarks" in another view with the ones I've favourited highlighted. First I used "#AppStorrage" but I was told too to use Core Data for it instead. So far I have the favourite button working in the LandmarkDetail view with "#AppStorage" but apparently I need to change that so it uses Core Data.
I've look around to get an understanding of how to do it with Core Data but I could really use a helping hand if anyone can help. I've already seen and read some tutorials about core data and how to set it up, but I can't find anything for my specific problem where I pull in data from a JSON and I need Core Data to handle the favourite feature.
Here is my code for the favourite button
struct FavoriteButton: View {
#AppStorage ("isFavorite") var isFavorite: Bool = false
var body: some View {
Button {
isFavorite.toggle()
} label: {
Label("Toggle Favorite", systemImage: isFavorite ? "star.fill" : "star")
.labelStyle(.iconOnly)
.foregroundColor(isFavorite ? .yellow : .gray)
}
}
}
Code from the landmark detail view
struct LandmarkDetail: View {
var body: some View {
ScrollView {
VStack(alignment: .leading) {
HStack {
Text(landmark.name)
.font(.title)
FavoriteButton()
}
}
}
}
}
Code for the rows in the list view
This is the one not working yet, so far it just pulls the data from a JSON.
MODEL
import Foundation
import SwiftUI
import CoreLocation
struct Landmark: Hashable, Codable, Identifiable {
var id: Int
var name: String
var park: String
var state: String
var description: String
var isFavorite: Bool
var isFeatured: Bool
var category: Category
enum Category: String, CaseIterable, Codable {
case lakes = "Lakes"
case rivers = "Rivers"
case mountains = "Mountains"
}
private var imageName: String
var image: Image{
Image(imageName)
}
var featureImage: Image? {
isFeatured ? Image(imageName + "_feature") : nil
}
private var coordinates: Coordinates
var locationCoordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: coordinates.latitude,
longitude: coordinates.longitude)
}
struct Coordinates: Hashable, Codable {
var latitude: Double
var longitude: Double
}
}
import Foundation
import Combine
final class ModelData: ObservableObject {
#Published var landmarks: [Landmark] = load("landmarkData.json")
var hikes: [Hike] = load("hikeData.json")
#Published var profile = Profile.default
var features: [Landmark] {
landmarks.filter { $0.isFeatured }
}
var categories: [String: [Landmark]] {
Dictionary(
grouping: landmarks,
by: { $0.category.rawValue }
)
}
}
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
First of all you need to create a .xcdatamodeld file named Landmarks. You can create it by pressing right button on the principal folder of your project and searching Data Model. After you need to create a new Entity named Landmark. You can add attributes showed in your model like id, name, park, etc... with their types. After you need to create a new Swift file in which you can create you Core Data Controller like this:
class DataController: ObservableObject {
let container = NSPersistentContainer(name: "Landmarks")
init() {
container.loadPersistentStores { description, error in
if let error = error {
print("Core Data failed to load: \(error.localizedDescription)")
}
}
}
}
Successively, you need to add to your LandmarksApp.swift file the following code:
struct LandmarksApp: App {
#StateObject private var dataController = DataController()
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, dataController.container.viewContext)
}
}
}
Continue adding this to your LandmarkDetail view:
struct LandmarkDetail: View {
#Environment(\.managedObjectContext) var moc
var body: some View {
ScrollView {
VStack(alignment: .leading) {
HStack {
Text(landmark.name)
.font(.title)
FavoriteButton()
}
}
}
}
}
To create a new item and add to Core Data you can write:
let landmark = Landmark(context: moc)
landmark.id = id
landmark.name = name
landmark.park = park
etc...
try? moc.save()
For your JSON data you can create a function that convert all JSON data in Core Data following these steps.
This question already has an answer here:
SwiftUI Loading Data
(1 answer)
Closed 1 year ago.
I'm trying to call an API on app launch with user's current location. My current code is:
In ContentView.swift:
struct ContentView: View {
#StateObject var locationManager = LocationManager()
var userLocation: String {
let latitude = "\(locationManager.lastLocation?.coordinate.latitude ?? 0)"
let longitude = "\(locationManager.lastLocation?.coordinate.longitude ?? 0)"
return "\(latitude),\(longitude)"
}
#ObservedObject var api = randomAPI(location: userLocation)
var body: some View {
...
In randomAPI.swift:
class randomAPI: ObservableObject {
init(location: String) {
callAPI(userLocation: location)
}
func callAPI(userLocation: String) {
...
And I'm getting the error:
Cannot use instance member 'userLocation' within property initializer; property initializers run before 'self' is available.
I don't know where should I put the line below to initialize the randomAPI. (I actually don't even know if this is the correct way to initialize it LOL)
#ObservedObject var api = randomAPI(location: userLocation)
Could someone help me with the problem? Thanks!
As Joakim mentioned, you need to use .onAppear. This is due to the fact that you cannot access a property before it is set in a struct.
For your case, the code would look like this:
struct ContentView: View {
#StateObject var locationManager = LocationManager()
var userLocation: String {
let latitude = "\(locationManager.lastLocation?.coordinate.latitude ?? 0)"
let longitude = "\(locationManager.lastLocation?.coordinate.longitude ?? 0)"
return "\(latitude),\(longitude)"
}
#ObservedObject var api: randomAPI?
var body: some View {
SomeView()
.onAppear {
api = randomAPI(location: userLocation)
}
...
However, I don't think setting an ObservedObject right after the view appears is the best way to do this.
If I were you, I would remove the init method in the rapidAPI class:
class randomAPI: ObservableObject {
func callAPI(userLocation: String) {
...
And call the callAPI method when the view appears:
struct ContentView: View {
#StateObject var locationManager = LocationManager()
var userLocation: String {
let latitude = "\(locationManager.lastLocation?.coordinate.latitude ?? 0)"
let longitude = "\(locationManager.lastLocation?.coordinate.longitude ?? 0)"
return "\(latitude),\(longitude)"
}
#ObservedObject var api = randomAPI()
var body: some View {
SomeView()
.onAppear {
api.callAPI(location: userLocation)
}
...
I have a problem with observed object in SwiftUI.
I can see changing values of observed object on the View struct.
However in class or function, even if I change text value of TextField(which is observable object) but "self.codeTwo.text still did not have changed.
here's my code sample (this is my ObservableObject)
class settingCodeTwo: ObservableObject {
private static let userDefaultTextKey = "textKey2"
#Published var text: String = UserDefaults.standard.string(forKey: settingCodeTwo.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: settingCodeTwo.userDefaultTextKey)
}
}
deinit {
canc.cancel()
}
}
and the main problem is... "self.codeTwo.text" never changed!
class NetworkManager: ObservableObject {
#ObservedObject var codeTwo = settingCodeTwo()
#Published var posts = [Post]()
func fetchData() {
var urlComponents = URLComponents()
urlComponents.scheme = "http"
urlComponents.host = "\(self.codeTwo.text)" //This one I want to use observable object
urlComponents.path = "/mob_json/mob_json.aspx"
urlComponents.queryItems = [
URLQueryItem(name: "nm_sp", value: "UP_MOB_CHECK_LOGIN"),
URLQueryItem(name: "param", value: "1000|1000|\(Gpass.hahaha)")
]
if let url = urlComponents.url {
print(url)
let session = URLSession(configuration: .default)
let task = session.dataTask(with: url) { (data, response, error) in
if error == nil {
let decoder = JSONDecoder()
if let safeData = data {
do {
let results = try decoder.decode(Results.self, from: safeData)
DispatchQueue.main.async {
self.posts = results.Table
}
} catch {
print(error)
}
}
}
}
task.resume()
}
}
}
and this is view, I can catch change of the value in this one
import SwiftUI
import Combine
struct SettingView: View {
#ObservedObject var codeTwo = settingCodeTwo()
var body: some View {
ZStack {
Rectangle().foregroundColor(Color.white).edgesIgnoringSafeArea(.all).background(Color.white)
VStack {
TextField("test", text: $codeTwo.text).textFieldStyle(BottomLineTextFieldStyle()).foregroundColor(.blue)
Text(codeTwo.text)
}
}
}
}
Help me please.
Non-SwiftUI Code
Use ObservedObject only for SwiftUI, your function / other non-SwiftUI code will not react to the changes.
Use a subscriber like Sink to observe changes to any publisher. (Every #Published variable has a publisher as a wrapped value, you can use it by prefixing with $ sign.
Reason for SwiftUI View not reacting to class property changes:
struct is a value type so when any of it's properties change then the value of the struct has changed
class is a reference type, when any of it's properties change, the underlying class instance is still the same.
If you assign a new class instance then you will notice that the view reacts to the change.
Approach:
Use a separate view and that accepts codeTwoText as #Binding that way when the codeTwoText changes the view would update to reflect the new value.
You can keep the model as a class so no changes there.
Example
class Model : ObservableObject {
#Published var name : String //Ensure the property is `Published`.
init(name: String) {
self.name = name
}
}
struct NameView : View {
#Binding var name : String
var body: some View {
return Text(name)
}
}
struct ContentView: View {
#ObservedObject var model : Model
var body: some View {
VStack {
Text("Hello, World!")
NameView(name: $model.name) //Passing the Binding to name
}
}
}
Testing
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let model = Model(name: "aaa")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
model.name = "bbb"
}
return ContentView(model: model)
}
}
It is used two different instances of SettingCodeTwo - one in NetworkNamager another in SettingsView, so they are not synchronised if created at same time.
Here is an approach to keep those two instances self-synchronised (it is possible because they use same storage - UserDefaults)
Tested with Xcode 11.4 / iOS 13.4
Modified code below (see also important comments inline)
extension UserDefaults {
#objc dynamic var textKey2: String { // helper keypath
return string(forKey: "textKey2") ?? ""
}
}
class SettingCodeTwo: ObservableObject { // use capitalised name for class !!!
private static let userDefaultTextKey = "textKey2"
#Published var text: String = UserDefaults.standard.string(forKey: SettingCodeTwo.userDefaultTextKey) ?? ""
private var canc: AnyCancellable!
private var observer: NSKeyValueObservation!
init() {
canc = $text.debounce(for: 0.2, scheduler: DispatchQueue.main).sink { newText in
UserDefaults.standard.set(newText, forKey: SettingCodeTwo.userDefaultTextKey)
}
observer = UserDefaults.standard.observe(\.textKey2, options: [.new]) { _, value in
if let newValue = value.newValue, self.text != newValue { // << avoid cycling on changed self
self.text = newValue
}
}
}
}
class NetworkManager: ObservableObject {
var codeTwo = SettingCodeTwo() // no #ObservedObject needed here
...
SwiftUI and Combine noob here, I isolated in a playground the problem I am having. Here is the playground.
final class ReactiveContainer<T: Equatable> {
#Published var containedValue: T?
}
class AppContainer {
static let shared = AppContainer()
let text = ReactiveContainer<String>()
}
struct TestSwiftUIView: View {
#State private var viewModel = "test"
var body: some View {
Text("\(viewModel)")
}
init(textContainer: ReactiveContainer<String>) {
textContainer.$containedValue.compactMap {
print("compact map \($0)")
return $0
}.assign(to: \.viewModel, on: self)
}
}
AppContainer.shared.text.containedValue = "init"
var testView = TestSwiftUIView(textContainer: AppContainer.shared.text)
print(testView)
print("Executing network request")
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
AppContainer.shared.text.containedValue = "Hello world"
print(testView)
}
When I run the playground this is what's happening:
compact map Optional("init")
TestSwiftUIView(_viewModel: SwiftUI.State<Swift.String>(_value: "test", _location: nil))
Executing network request
TestSwiftUIView(_viewModel: SwiftUI.State<Swift.String>(_value: "test", _location: nil))
So as you can see, two problems there:
The compact map closure is only called once, on subscription but not when the dispatch is ran
The assign operator is never called
I have been trying to solve this these past few hours without any success. Maybe someone with a top knowledge in SwiftUI/Combine could help me, thx !
EDIT
Here is the working solution:
struct ContentView: View {
#State private var viewModel = "test"
let textContainer: ReactiveContainer<String>
var body: some View {
Text(viewModel).onReceive(textContainer.$containedValue) { (newContainedValue) in
self.viewModel = newContainedValue ?? ""
}
}
init(textContainer: ReactiveContainer<String>) {
self.textContainer = textContainer
}
}
I would prefer to use ObservableObject/ObservedObject pattern, right below, but other variants also possible (as provided further)
All tested with Xcode 11.2 / iOS 13.2
final class ReactiveContainer<T: Equatable>: ObservableObject {
#Published var containedValue: T?
}
struct TestSwiftUIView: View {
#ObservedObject var vm: ReactiveContainer<String>
var body: some View {
Text("\(vm.containedValue ?? "<none>")")
}
init(textContainer: ReactiveContainer<String>) {
self._vm = ObservedObject(initialValue: textContainer)
}
}
Alternates:
The following fixes your case (if you don't store subscriber the publisher is canceled immediately)
private var subscriber: AnyCancellable?
init(textContainer: ReactiveContainer<String>) {
subscriber = textContainer.$containedValue.compactMap {
print("compact map \($0)")
return $0
}.assign(to: \.viewModel, on: self)
}
Please note, view's state is linked only being in view hierarchy, in Playground like you did it holds only initial value.
Another possible approach, that fits better for SwiftUI hierarchy is
struct TestSwiftUIView: View {
#State private var viewModel: String = "test"
var body: some View {
Text("\(viewModel)")
.onReceive(publisher) { value in
self.viewModel = value
}
}
let publisher: AnyPublisher<String, Never>
init(textContainer: ReactiveContainer<String>) {
publisher = textContainer.$containedValue.compactMap {
print("compact map \($0)")
return $0
}.eraseToAnyPublisher()
}
}
I would save a reference to AppContainer.
struct TestSwiftUIView: View {
#State private var viewModel = "test"
///I just added this
var textContainer: AnyCancellable?
var body: some View {
Text("\(viewModel)")
}
init(textContainer: ReactiveContainer<String>) {
self.textContainer = textContainer.$containedValue.compactMap {
print("compact map \(String(describing: $0))")
return $0
}.assign(to: \.viewModel, on: self)
}
}
compact map Optional("init")
TestSwiftUIView(_viewModel: SwiftUI.State<Swift.String>(_value: "test", _location: nil), textContainer: Optional(Combine.AnyCancellable))
Executing network request
compact map Optional("Hello")
TestSwiftUIView(_viewModel: SwiftUI.State<Swift.String>(_value: "test", _location: nil), textContainer: Optional(Combine.AnyCancellable))
We don't use Combine for moving data between Views, SwiftUI already has built-in support for this. The main problem is you are treating the TestSwiftUIView as if it is a class but it is a struct, i.e. a value. It's best to think of the View simply as the data to be displayed. SwiftUI creates these data structs over and over again when data changes. So the solution is simply:
struct ContentView: View {
let text: String
var body: some View { // only called if text is different from last time ContentView was created in a parent View's body.
Text(text)
}
}
The parent body method can call ContentView(text:"Test") over and over again but the ContentView body method is only called by SwiftUI when the let text is different from last time, e.g. ContentView(text:"Test2"). I think this is what you tried to recreate with Combine but it is unnecessary because SwiftUI already does it.
I’m developing an iOS application with SwiftUI, and I’m having trouble displaying my data fetched from database.
Code
import SwiftUI
import Firebase
import FirebaseDatabase
var ref = Database.database().reference()
class Observe {
static func currentSingleEventObserve(completion: #escaping ((String?) -> ())) {
let path = "supersonic/current"
let ref = Database.database().reference().child(path)
_ = ref.observeSingleEvent(of: .value, with: { snapshot in
let temp = (snapshot.value! as AnyObject).description
completion(temp)
})
}
}
struct CurrentDistance: View {
var value: NSDictionary?
var refHandle: UInt = 0
#State var distance: String
init() {
Observe.currentSingleEventObserve(completion: { temp in
self.distance = temp // I want to mutate self.distance here
})
}
var body: some View {
VStack {
Text("Distance")
Text(self.distance)
}
}
}
struct CurrentDistance_Previews: PreviewProvider {
static var previews: some View {
CurrentDistance()
}
}
Database
|-supersonic
|- current: 30
Problem
I want to mutate self.distance in the initializer. Trying to mutate self.distance in the closure I got an error Escaping closure captures mutating 'self' parameter, and I don't know how to update the value.
How can I display the value fetched from the database?
This is a somewhat generic answer as we are just going to be updating a UI element based on a value read from Firebase.
Firebase is asynchronous and values are only valid following the Firebase function, within the closure.
Suppose we have a simple SwiftUI app that displays a Text object, a button to click to load the data from Firebase, and then a var that holds what the text should be.
struct ContentView: View {
#State var buttonText = "Initial Button Label"
var body: some View {
VStack {
Text(buttonText)
Button(action: {
self.readFirebase()
}) {
Text("Click Me!")
}
}
}
func readFirebase() {
let ref = my_firebase_ref
let textRef = ref.child("string_node")
textRef.observeSingleEvent(of: .value, with: { snapshot in
let myText = snapshot.value as? String ?? "No String"
self.buttonText = myText
})
}
}
Here's my Firebase structure
root
string_node: "Hello, World"