How to make a view conform to MapAnnotationProtocol - swiftui

I have a tappable MapAnnotation and whenever I try to customize the "look" of the annotation pin, I get the following error:
Initializer 'init(coordinateRegion:interactionModes:showsUserLocation:userTrackingMode:annotationItems:annotationContent:)' requires that 'NavigationLink<PlaceAnnotationView, LocationDetailsView>' conform to 'MapAnnotationProtocol'
Annotation is defined by this:
struct Annotation: Codable, Identifiable, Hashable {
#DocumentID var id: String?
let lat: String?
let lng: String?
var name: String
}
And the Map is defined by this:
Map(coordinateRegion: $region, annotationItems: annotations) { place in
NavigationLink {
LocationDetailsView(place: place.name)
} label: {
PlaceAnnotationView(title: place.name)
}
}
Have I forgotten to define something? Please let me know. Thanks.

https://developer.apple.com/documentation/mapkit/mapannotationprotocol
Don’t create types conforming to MapAnnotationProtocol protocol. Instead, use one of the framework-provided types MapAnnotation, MapMarker, and MapPin.
https://developer.apple.com/documentation/mapkit/mapannotation

Related

swiftui with mixed picker

I'm trying to make a mixed picker in swift ui. Means: first part is made of values from a constant, second part is made of core data entity values.
My Code so far
Structs
struct StandardArten: Identifiable, Hashable {
var id: UUID = UUID()
var name: String
}
struct Constants {
static let standardKontakte = [
StandardArten(name: "Hausarzt")
]
}
picker
Picker("Kategorie", selection: $kategorie) {
Text("Bitte wählen...").tag(Optional<UUID>(nil))
ForEach(Constants.standardKontakte, id: \.id) { kat in
Text(kat.name).tag(kat.id)
}
Divider()
ForEach(adressKategorien, id:\.id) { kat in
Text(kat.name ?? "ohne Titel").tag(kat.id)
}
}
The picker is displayed properly (see image), but selecting "Hausarzt" the picker "jumps back" to the old value.
So what am I doing wrong here?
Additional information:
$kategorie is declared as #State var kategorie: UUID?

How to loop over #AppStorage values and use them for creating TextFields?

I am trying to loop over #AppStorage variables and use them to create TextFields with predefined values. I've tried several things as adding and removing $ for variable1-5 and variable, but nothing seems to work.
To make it easier I reduced the amount of variables and removed all the unneeded code.
import SwiftUI
struct ContentView: View {
static let store = UserDefaults(suiteName: "group.not-important.etc")
#AppStorage("variable1", store: store) var variable1 = "a"
#AppStorage("variable2", store: store) var variable2 = "b"
#AppStorage("variable3", store: store) var variable3 = "c"
#AppStorage("variable4", store: store) var variable4 = "d"
#AppStorage("variable5", store: store) var variable5 = "e"
var body: some View {
NavigationStack {
Form {
Section(header: Text("Variables")) {
HStack (alignment: .center) {
ForEach([
$variable1, // also tried variable1, etc...
$variable2,
$variable3,
$variable4,
$variable5
], id: \.self) { variable in // also tried $variable
TextField(
"",
text: variable // also tried $variable, variable.wrappedName, variable.text, etc...
)
}
}
}
}
}
}
}
Please help to understand what is the issue here and why I can not get it to work.
More info on errors here:
Provided example throws an error for ForEach line:
Generic struct 'ForEach' requires that 'Binding<String>' conform to 'Hashable'
When trying $variable in instead of variable in I get:
Inferred projection type 'String' is not a property wrapper
TextField("", text: $variable) throws:
Cannot convert value of type 'String' to expected argument type 'Binding<String>'
I think the main issue with your attempts, is that you are trying to create
an array of variables, but an array does not accept that, it accepts values,
String, Int, custom types etc..., but not variables. This is why it does not "work".
Since you may have lots of variables that you want to process in a loop,
I suggest you use an array instead. This makes looping very easy,
as shown in this example code. You can still have other independent variables in your code.
Here is the example code, using the Array extension to easily store the array in AppStorage, from: SwiftUI: What is #AppStorage property wrapper
struct ContentView: View {
static let store = UserDefaults(suiteName: "group.not-important.etc")
#AppStorage("arr", store: store) var arr: [String] = ["a","b","c","d","e"]
var body: some View {
Form {
Section(header: Text("Variables")) {
HStack (alignment: .center) {
ForEach(arr.indices, id: \.self) { index in
TextField("", text: $arr[index])
.border(.red) // <-- for testing
}
}
}
}
}
}
extension Array: RawRepresentable where Element: Codable {
public init?(rawValue: String) {
guard let data = rawValue.data(using: .utf8),
let result = try? JSONDecoder().decode([Element].self, from: data)
else {
return nil
}
self = result
}
public var rawValue: String {
guard let data = try? JSONEncoder().encode(self),
let result = String(data: data, encoding: .utf8)
else {
return "[]"
}
return result
}
}

Generic SwiftUI Component cannot infer Hashable for CustomStringConvertible

I want to create a generic type, that accepts anything that conforms to CustomStringConvertible and then iterates over those items.
Here is an example that distils down that problem:
public struct Test<ItemType: CustomStringConvertible, Hashable>: View {
var items: [ItemType]
public var body: some View {
ForEach(items, id: \.self) { item in
Text("test")
}
}
}
let items: [String] = ["a", "b"]
let viewController = UIHostingController(rootView: Test(items: items))
So I get an error
Generic struct 'ForEach' requires that 'ItemType' conform to 'Hashable'
and
Generic parameter 'Hashable' could not be inferred
So what am I doing wrong?
You have syntax issue:
public struct Test<ItemType: CustomStringConvertible & Hashable>: View { // <<: here!
var items: [ItemType]
public var body: some View {
ForEach(items, id: \.self) { item in
Text("test")
}
}
}

How make work a Picker with an ObservedObject in SwiftUI?

I'm trying to get a list of Datacenters from a Rest API and show them in a Picker, so the user can choose one. When I do it with a static list it works fine. However, retrieving the Datacenters dinamically seems not to work fine.
I'm using Xcode 11 (GM)
This is the Datacenter Object
struct Datacenter:Codable, Hashable, Identifiable{
let id: String
var location: String
}
This is the ObservedObject (it has the property datacenters that is an array of Datacenter objects)
#ObservedObject var datacenters_controller : DatacentersController
#State private var selectedDatacenter = 0
This was my first attempt:
Picker(selection: $selectedDatacenter, label: Text("Datacenter")) {
ForEach(0 ..< datacenters_controller.datacenters.count) {
Text(self.datacenters_controller.datacenters[$0].location)
}
}
Swift complained with the following error:
ForEach<Range<Int>, Int, Text> count (4) != its initial count (0). `ForEach(_:content:)` should only be used for *constant* data. Instead conform data to `Identifiable` or use `ForEach(_:id:content:)` and provide an explicit `id`!
Then I switched to:
Picker(selection: $selectedDatacenter, label: Text("Datacenter")) {
ForEach(datacenters_controller.datacenters) { datacenter in
Text(datacenter.location)
}
}
It "works" (no error), but the result is not the expected because although I can select a datacenter, it is not "stored", not shown in the Picker as selected.
Actual result
Expected result
Any idea? What I'm doing wrong?
Here's a working example. The key is that selectedDatacenter needs to be the same type as Datacenter.id (in this case, String).
struct ContentView: View {
#ObservedObject var datacenters_controller = DatacentersController()
#State private var selectedDatacenter = ""
var body: some View {
NavigationView {
Form {
Picker(selection: $selectedDatacenter, label: Text("Datacenter")) {
ForEach(datacenters_controller.datacenters) { datacenter in
Text(datacenter.location)
}
}
// Just here for demonstration
Text("selectedDatacenter (id): \(selectedDatacenter.isEmpty ? "Nothing yet" : selectedDatacenter)")
}
}
}
}
Here's the supporting code
struct Datacenter:Codable, Hashable, Identifiable{
let id: String
var location: String
}
class DatacentersController: ObservableObject {
#Published var datacenters: [Datacenter] = []
init() {
datacenters = [
Datacenter(id: "ABQ", location: "Albuquerque"),
Datacenter(id: "BOS", location: "Boston"),
Datacenter(id: "COS", location: "Colorado Springs")
]
}
}
I think you are missing tag on your picker:
Picker(selection: $selectedDatacenter, label: Text("Datacenter")) {
ForEach(datacenters_controller.datacenters) {
Text($0.location).tag($0)
}
}
Apple docs on tag:
Sets the tag of the view, used for selecting from a list of View
options.
In your second attempt, you need to use the tag modifier (as described by LuLuGaGa). You also need to change the type of selectedDatacenter to match. For example:
struct ContentView: View {
init(_ controller: DatacentersController) {
self.datacenters_controller = controller
self._selectedDatacenter = State(initialValue: controller.datacenters[0].id)
}
var body: some View {
NavigationView {
Form {
Picker(selection: $selectedDatacenter, label: Text("Datacenter")) {
ForEach(datacenters_controller.datacenters) {
Text($0.location).tag($0)
}
}
}
}
}
#ObservedObject private var datacenters_controller: DatacentersController
#State private var selectedDatacenter: String
}

How do I access data from a child view as the parent view at any time in SwiftUI?

I'm new to SwiftUI and understand that I may need to implement EnvironmentObject in some way, but I'm not sure how in this case.
This is the Trade class
class Trade {
var teamsSelected: [Team]
init(teamsSelected: [Team]) {
self.teamsSelected = teamsSelected
}
}
This is the child view. It has an instance trade from the Trade class. There is a button that appends 1 to array teamsSelected.
struct TeamRow: View {
var trade: Trade
var body: some View {
Button(action: {
self.trade.teamsSelected.append(1)
}) {
Text("Button")
}
}
}
This is the parent view. As you can see, I pass trade into the child view TeamRow. I want trade to be in sync with trade in TeamRow so that I can then pass trade.teamsSelected to TradeView.
struct TeamSelectView: View {
var trade = Trade(teamsSelected: [])
var body: some View {
NavigationView{
VStack{
NavigationLink(destination: TradeView(teamsSelected: trade.teamsSelected)) {
Text("Trade")
}
List {
ForEach(teams) { team in
TeamRow(trade: self.trade)
}
}
}
}
}
}
I've taken your code and changed some things to illustrate how SwiftUI works in order to give you a better understanding of how to use ObservableObject, #ObservedObject, #State, and #Binding.
One thing to mention up front - #ObservedObject is currently broken when trying to run SwiftUI code on a physical device running iOS 13 Beta 6, 7, or 8. I answered a question here that goes into that in more detail and explains how to use #EnvironmentObject as a workaround.
Let's first take a look at Trade. Since you're looking to pass a Trade object between views, change properties on that Trade object, and then have those changes reflected in every view that uses that Trade object, you'll want to make Trade an ObservableObject. I've added an extra property to your Trade class purely for illustrative purposes that I'll explain later. I'm going to show you two ways to write an ObservableObject - the verbose way first to see how it works, and then the concise way.
import SwiftUI
import Combine
class Trade: ObservableObject {
let objectWillChange = PassthroughSubject<Void, Never>()
var name: String {
willSet {
self.objectWillChange.send()
}
}
var teamsSelected: [Int] {
willSet {
self.objectWillChange.send()
}
}
init(name: String, teamsSelected: [Int]) {
self.name = name
self.teamsSelected = teamsSelected
}
}
When we conform to ObservableObject, we have the option to write our own ObservableObjectPublisher, which I've done by importing Combine and creating a PassthroughSubject. Then, when I want to publish that my object is about to change, I can call self.objectWillChange.send() as I have on willSet for name and teamsSelected.
This code can be shortened significantly, however. ObservableObject automatically synthesizes an object publisher, so we don't actually have to declare it ourselves. We can also use #Published to declare our properties that should send a publisher event instead of using self.objectWillChange.send() in willSet.
import SwiftUI
class Trade: ObservableObject {
#Published var name: String
#Published var teamsSelected: [Int]
init(name: String, teamsSelected: [Int]) {
self.name = name
self.teamsSelected = teamsSelected
}
}
Now let's take a look at your TeamSelectView, TeamRow, and TradeView. Keep in mind once again that I've made some changes (and added an example TradeView) just to illustrate a couple of things.
struct TeamSelectView: View {
#ObservedObject var trade = Trade(name: "Name", teamsSelected: [])
#State var teams = [1, 1, 1, 1, 1]
var body: some View {
NavigationView{
VStack{
NavigationLink(destination: TradeView(trade: self.trade)) {
Text(self.trade.name)
}
List {
ForEach(self.teams, id: \.self) { team in
TeamRow(trade: self.trade)
}
}
Text("\(self.trade.teamsSelected.count)")
}
.navigationBarItems(trailing: Button("+", action: {
self.teams.append(1)
}))
}
}
}
struct TeamRow: View {
#ObservedObject var trade: Trade
var body: some View {
Button(action: {
self.trade.teamsSelected.append(1)
}) {
Text("Button")
}
}
}
struct TradeView: View {
#ObservedObject var trade: Trade
var body: some View {
VStack {
Text("\(self.trade.teamsSelected.count)")
TextField("Trade Name", text: self.$trade.name)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
}
}
}
Let's first look at #State var teams. We use #State for simple value types - Int, String, basic structs - or collections of simple value types. #ObservedObject is used for objects that conform to ObservableObject, which we use for data structures that are more complex than just Int or String.
What you'll notice with #State var teams is that I've added a navigation bar item that will append a new item to the teams array when pressed, and since our List is generated by iterating through that teams array, our view re-renders and adds a new item to our List whenever the button is pressed. That's a very basic example of how you would use #State.
Next, we have our #ObservedObject var trade. You'll notice that I'm not really doing anything different than you were originally. I'm still creating an instance of my Trade class and passing that instance between my views. But since it's now an ObservableObject, and we're using #ObservedObject, our views will now all receive publisher events whenever the Trade object changes and will automatically re-render their views to reflect those changes.
The last thing I want to point out is the TextField I created in TradeView to update the Trade object's name property.
TextField("Trade Name", text: self.$trade.name)
The $ character indicates that I'm passing a binding to the text field. This means that any changes TextField makes to name will be reflected in my Trade object. You can do the same thing yourself by declaring #Binding properties that allow you to pass bindings between views when you are trying to sync state between your views without passing entire objects.
While I changed your TradeView to take #ObservedObject var trade, you could simply pass teamsSelected to your trade view as a binding like this - TradeView(teamsSelected: self.$trade.teamsSelected) - as long as your TradeView accepts a binding. To configure your TradeView to accept a binding, all you would have to do is declare your teamsSelected property in TradeView like this:
#Binding var teamsSelected: [Team]
And lastly, if you run into issues with using #ObservedObject on a physical device, you can refer to my answer here for an explanation of how to use #EnvironmentObject as a workaround.
You can use #Binding and #State / #Published in Combine.
In other words, use a #Binding property in Child View and bind it with a #State or a #Published property in Parent View as following.
struct ChildView: View {
#Binding var property1: String
var body: some View {
VStack(alignment: .leading) {
TextField(placeholderTitle, text: $property1)
}
}
}
struct PrimaryTextField_Previews: PreviewProvider {
static var previews: some View {
PrimaryTextField(value: .constant(""))
}
}
struct ParentView: View{
#State linkedProperty: String = ""
//...
ChildView(property1: $linkedProperty)
//...
}
or if you have a #Publilshed property in your viewModel(#ObservedObject), then use it to bind the state like ChildView(property1: $viewModel.publishedProperty).
firstly thanks a lot to graycampbell for giving me a better understanding! However, my understanding does not seem to work out completely. I have a slightly different case which I'm not fully able to solve.
I've already asked my question in a separate thread, but I want to add it here as well, because it somehow fits the topic: Reading values from list of toggles in SwiftUI
Maybe somebody of you guys can help me with this. The main difference to the initial post if this topic is, that I have to collect Data from each GameGenerationRow in the GameGenerationView and then hand it over to another View.