NEW: Join my free 100 Days of SwiftUI challenge today! >>

Extending existing types to support ObservableObject

Paul Hudson    @twostraws   

Users can now drop pins on our MapView, but they can’t do anything with them – they can’t attach their own title and subtitle. Fixing this requires a little bit of thinking, because MKPointAnnotation uses optional strings for title and subtitle, and SwiftUI doesn’t let us bind optionals to text fields.

There are a couple of ways of fixing this, but the easiest one by far is writing an extension to MKPointAnnotation that adds computed properties around title and subtitle, which means we can then make the class conform to ObservableObject without any further work. You can call these computed properties whatever you want – name, info, details, etc – but you’ll probably find that marking them as simple wrappers works out easier to remember in the long term, which is why I’m going to use the names wrappedTitle and wrappedSubtitle.

Create a new Swift file called MKPointAnnotation-ObservableObject, change its Foundation import for MapKit, then give it this code:

extension MKPointAnnotation: ObservableObject {
    public var wrappedTitle: String {
        get {
            self.title ?? "Unknown value"
        }

        set {
            title = newValue
        }
    }

    public var wrappedSubtitle: String {
        get {
            self.subtitle ?? "Unknown value"
        }

        set {
            subtitle = newValue
        }
    }
}

Notice how I haven’t marked those computed properties as @Published? This is OK here because we won’t actually be reading the properties as they are being changed, so there’s no need to keep refreshing the view as the user types.

With that new extension in place, we have two properties on MKPointAnnotation that aren’t optional, which means we can now bind some UI controls to them in a SwiftUI view – we can create a UI for editing place marks.

As always we’re going to start small and work our way up, so please create a new SwiftUI view called “EditView”, add an import for MapKit, then give it this code:

struct EditView: View {
    @Environment(\.presentationMode) var presentationMode
    @ObservedObject var placemark: MKPointAnnotation

    var body: some View {
        NavigationView {
            Form {
                Section {
                    TextField("Place name", text: $placemark.wrappedTitle)
                    TextField("Description", text: $placemark.wrappedSubtitle)
                }
            }
            .navigationBarTitle("Edit place")
            .navigationBarItems(trailing: Button("Done") {
                self.presentationMode.wrappedValue.dismiss()
            })
        }
    }
}

Make you update the preview code so that it passes in our example MKPointAnnotation, like this:

struct EditView_Previews: PreviewProvider {
    static var previews: some View {
        EditView(placemark: MKPointAnnotation.example)
    }
}

We want to display that in two places, both in ContentView: when the user adds a place we want them to immediately edit it, and when they press the Edit button in our pin alert.

Both of these will be triggered by a Boolean condition, so start by adding this @State property to ContentView:

@State private var showingEditScreen = false

That should be set to true when the user taps Edit in our alert, which means replacing the // edit this place comment with this:

self.showingEditScreen = true

And it also means setting it to true when they just added a new place to the map, but we also need to set the selectedPlace property so our code knows which place should be edited. So, put this below the self.locations.append(newLocation) line:

self.selectedPlace = newLocation
self.showingEditScreen = true

And finally, we need to bind showingEditScreen to a sheet, so our EditView struct gets presented with a place mark at the right time. Remember, we can’t use if let here to unwrap the selectedPlace optional, so we’ll do a simple check then force unwrap – it’s just as safe.

Please attach this sheet() modifier to ContentView, after the existing alert:

.sheet(isPresented: $showingEditScreen) {
    if self.selectedPlace != nil {
        EditView(placemark: self.selectedPlace!)
    }
}

That’s the next step of our app done, and it’s now almost useful – you can browse the map, tap to drop pins, then give them a meaningful title and subtitle.

LEARN SWIFTUI FOR FREE I have a massive, free SwiftUI video collection on YouTube teaching you how to build complete apps with SwiftUI – check it out!

BUY OUR BOOKS
Buy Pro Swift Buy Swift Design Patterns Buy Testing Swift Buy Hacking with iOS Buy Swift Coding Challenges Buy Swift on Sundays Volume One Buy Server-Side Swift (Vapor Edition) Buy Advanced iOS Volume One Buy Advanced iOS Volume Two Buy Advanced iOS Volume Three Buy Hacking with watchOS Buy Hacking with tvOS Buy Hacking with macOS Buy Dive Into SpriteKit Buy Swift in Sixty Seconds Buy Objective-C for Swift Developers Buy Server-Side Swift (Kitura Edition) Buy Beyond Code

Was this page useful? Let us know!

Average rating: 5.0/5