100 Days of SwiftUI(DAY 72(Project14 part 5))

他の誰かのクラスをCodableに準拠させる
Making someone else’s class conform to Codable

私たちのアプリではMKPointAnnotation、ユーザーが訪れたい興味深い場所を保存するために使用しており、iOSストレージを使用して永続的に保存したいと考えています。MKPointAnnotation-Codable.swiftという新しいSwiftファイルを作成し、MapKitのインポートを追加して、次のコードを指定します。

MKPointAnnotationのサブクラスを作成して実装Codableし、使用されているMKPointAnnotation知識からを効果的に保護できますCodable。これが現在のクラスなので、サブクラスをに準拠させることができCodableます。

72日目保存したデータを読み込めません。

↑の前に、以下にて解決!!

newLocation.title = "Example location"
//追加
newLocation.subtitle = "Example subtitle"
newLocation.coordinate = self.centerCoordinate

//
//  MKPointAnnotation-Codable.swift
//  BucketList
//

import Foundation
import MapKit

class CodableMKPointAnnotation: MKPointAnnotation, Codable {
    
    enum CodingKeys: CodingKey {
        case title, subtitle, latitude, longitude
    }

    override init() {
        super.init()
    }

    public required init(from decoder: Decoder) throws {
        super.init()

        let container = try decoder.container(keyedBy: CodingKeys.self)
        title = try container.decode(String.self, forKey: .title)
        subtitle = try container.decode(String.self, forKey: .subtitle)

        let latitude = try container.decode(CLLocationDegrees.self, forKey: .latitude)
        let longitude = try container.decode(CLLocationDegrees.self, forKey: .longitude)
        coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
    }

    public func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(title, forKey: .title)
        try container.encode(subtitle, forKey: .subtitle)
        try container.encode(coordinate.latitude, forKey: .latitude)
        try container.encode(coordinate.longitude, forKey: .longitude)
    }
}

import SwiftUI
import MapKit

struct ContentView: View {
    //中心座標
    @State private var centerCoordinate = CLLocationCoordinate2D()
    //Annotation配列
    //@State private var locations = [MKPointAnnotation]()
    @State private var locations = [CodableMKPointAnnotation]()
    //plus(+)button押下 -->> sheet表示用
    //mapviewのボタン(calloutAccessoryControlTapped)押下のAnnotationを特定
    @State private var selectedPlace: MKPointAnnotation?
    
    //show Alert
    @State private var showingPlaceDetails = false
    //show sheet
    @State private var showingEditScreen = false
    //アプリのドキュメントディレクトリを見つける
    func getDocumentsDirectory() -> URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        return paths[0]
    }
    //-------------
    func loadData() {
        let filename = getDocumentsDirectory().appendingPathComponent("SavedPlaces")

        do {
            let data = try Data(contentsOf: filename)
            locations = try JSONDecoder().decode([CodableMKPointAnnotation].self, from: data)
        } catch {
            print("Unable to load saved data.")
        }
    }
    //-------------
    func saveData() {
        do {
            let filename = getDocumentsDirectory().appendingPathComponent("SavedPlaces")
            let data = try JSONEncoder().encode(self.locations)
            try data.write(to: filename, options: [.atomicWrite, .completeFileProtection])
        } catch {
            print("Unable to save data.")
        }
    }
    var body: some View {
        ZStack {
            //引数として、中心座標、
            MapView(centerCoordinate: $centerCoordinate, selectedPlace: $selectedPlace, showingPlaceDetails: $showingPlaceDetails, annotations: locations)
                .edgesIgnoringSafeArea(.all)
            Circle()
                .fill(Color.blue)
                .opacity(0.3)
                .frame(width: 32, height: 32)
            VStack {
                Spacer()
                HStack {
                    Spacer()
                    Button(action: {
                        // create a new location
                        //let newLocation = MKPointAnnotation()
                        let newLocation = CodableMKPointAnnotation()
                        newLocation.title = "Example location"
                        //追加
                        newLocation.subtitle = "Example subtitle"
                        newLocation.coordinate = self.centerCoordinate
                        self.locations.append(newLocation)
                        self.selectedPlace = newLocation
                        self.showingEditScreen = true
                    }) {
                        Image(systemName: "plus")
                    }
                    .padding()
                    .background(Color.black.opacity(0.75))
                    .foregroundColor(.white)
                    .font(.title)
                    .clipShape(Circle())
                    .padding(.trailing)
                }
            }
        }
        .alert(isPresented: $showingPlaceDetails) {
            Alert(title: Text(selectedPlace?.title ?? "Unknown"), message: Text(selectedPlace?.subtitle ?? "Missing place information."), primaryButton: .default(Text("OK")), secondaryButton: .default(Text("Edit")) {
                // edit this place
                self.showingEditScreen = true
            })
        }
        .sheet(isPresented: $showingEditScreen, onDismiss: saveData) {
            if self.selectedPlace != nil {
                EditView(placemark: self.selectedPlace!)
            }
        }
        //UserDefaultsからlocations読み込み
        .onAppear(perform: loadData)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}


Face IDの背後にあるUIをロックする
Locking our UI behind Face ID

Info.plist
we need to add the “Privacy – Face ID Usage Description” key to Info.plist, explaining to the user why we want to use Face ID.You can enter what you like, but “Please authenticate yourself to unlock your places” seems like a good choice.

//
//  ContentView.swift
//  BucketList
//

import SwiftUI
import MapKit
import LocalAuthentication

struct ContentView: View {
    @State private var isUnlocked = false
    //中心座標
    @State private var centerCoordinate = CLLocationCoordinate2D()
    //Annotation配列
    //@State private var locations = [MKPointAnnotation]()
    @State private var locations = [CodableMKPointAnnotation]()
    //plus(+)button押下 -->> sheet表示用
    //mapviewのボタン(calloutAccessoryControlTapped)押下のAnnotationを特定
    @State private var selectedPlace: MKPointAnnotation?
    
    //show Alert
    @State private var showingPlaceDetails = false
    //show sheet
    @State private var showingEditScreen = false
    //アプリのドキュメントディレクトリを見つける
    func getDocumentsDirectory() -> URL {
        let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
        return paths[0]
    }
    //-------------
    func loadData() {
        let filename = getDocumentsDirectory().appendingPathComponent("SavedPlaces")

        do {
            let data = try Data(contentsOf: filename)
            locations = try JSONDecoder().decode([CodableMKPointAnnotation].self, from: data)
        } catch {
            print("Unable to load saved data.")
        }
    }
    //-------------
    func saveData() {
        do {
            let filename = getDocumentsDirectory().appendingPathComponent("SavedPlaces")
            let data = try JSONEncoder().encode(self.locations)
            try data.write(to: filename, options: [.atomicWrite, .completeFileProtection])
        } catch {
            print("Unable to save data.")
        }
    }
    //-------------
    func authenticate() {
        let context = LAContext()
        var error: NSError?

        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
            let reason = "Please authenticate yourself to unlock your places."

            context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, authenticationError in

                DispatchQueue.main.async {
                    if success {
                        self.isUnlocked = true
                    } else {
                        // error
                    }
                }
            }
        } else {
            // no biometrics
        }
    }
    var body: some View {
        ZStack {
            if isUnlocked {
                //引数として、中心座標、
                MapView(centerCoordinate: $centerCoordinate, selectedPlace: $selectedPlace, showingPlaceDetails: $showingPlaceDetails, annotations: locations)
                    .edgesIgnoringSafeArea(.all)
                Circle()
                    .fill(Color.blue)
                    .opacity(0.3)
                    .frame(width: 32, height: 32)
                VStack {
                    Spacer()
                    HStack {
                        Spacer()
                        Button(action: {
                            // create a new location
                            //let newLocation = MKPointAnnotation()
                            let newLocation = CodableMKPointAnnotation()
                            newLocation.title = "Example location"
                            //追加
                            newLocation.subtitle = "Example subtitle"
                            newLocation.coordinate = self.centerCoordinate
                            self.locations.append(newLocation)
                            self.selectedPlace = newLocation
                            self.showingEditScreen = true
                        }) {
                            Image(systemName: "plus")
                        }
                        .padding()
                        .background(Color.black.opacity(0.75))
                        .foregroundColor(.white)
                        .font(.title)
                        .clipShape(Circle())
                        .padding(.trailing)
                    }
                }
            } else {
                // button here
                Button("Unlock Places") {
                    self.authenticate()
                }
                .padding()
                .background(Color.blue)
                .foregroundColor(.white)
                .clipShape(Capsule())
            }
        }
        .alert(isPresented: $showingPlaceDetails) {
            Alert(title: Text(selectedPlace?.title ?? "Unknown"), message: Text(selectedPlace?.subtitle ?? "Missing place information."), primaryButton: .default(Text("OK")), secondaryButton: .default(Text("Edit")) {
                // edit this place
                self.showingEditScreen = true
            })
        }
        .sheet(isPresented: $showingEditScreen, onDismiss: saveData) {
            if self.selectedPlace != nil {
                EditView(placemark: self.selectedPlace!)
            }
        }
        //UserDefaultsからlocations読み込み
        .onAppear(perform: loadData)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}


これは今までで最大のプロジェクトでしたが、Comparableカスタムタイプへの追加、ドキュメントディレクトリの検索、MapKitの統合、生体認証の使用、安全なData書き込みなど、多くのことをカバーしました。

任意の-これとInstafilterの間に、あなたは今、あなたのアプリにUIKitの任意の部分を埋め込む方法を見てきたUIViewか、UIViewController今SwiftUIの内側に配置することができます。これにより、UIKitの学習に時間を費やすことができる限り、作成できるアプリの種類が大幅に広がります。時間の経過とともに、SwiftUIはその機能を拡張して拡大しますが、現時点では、その限界を理解し、その強みを発揮することが重要です。