ikeh1024のブログ

ZennやQiitaに書くにはちょっと小粒なネタをこちらに書いています

iOS Apprentice Section3 My Locationsに関するメモ

iOS_Apprentice Chapter21

Collections

arrayの宣言や初期化は以下の通り

// An array of ChecklistItem objects:
var items: Array<ChecklistItem>
// Or, using shorthand notation:
var items: [ChecklistItem]
// Making an instance of the array:
items = [ChecklistItem]()

dictionary

// A dictionary that stores (String, Int) pairs, for example a
// list of people’s names and their ages:
var ages: Dictionary<String, Int>
// Or, using shorthand notation:
var ages: [String: Int]
// Making an instance of the dictionary:
ages = [String: Int]()
// Accessing an object from the dictionary:
var age = dict["Jony Ive"]

Properties

  • プロパティには2種類ある
    • Stored propertiesとComputed properties
  • Computed propertiesは計算を行って値を読んだり書き込むもの
class MyObject {
    var indexOfSelectedChecklist: Int {
        get {
            return UserDefaults.standard.integer(
                forKey: "ChecklistIndex")
        }
        set {
            UserDefaults.standard.set(newValue,
                                      forKey: "ChecklistIndex")
        }
    }
}

iOS_Apprentice Chapter22

Make project

  • P513にプロジェクト全体で行うことの概要の記載がある
  • この章で行うこと
    • Get GPS Coordinates: Create a tab bar-based app and set up the UI for the first tab.
    • CoreLocation: Use the CoreLocation framework to get the user's current location.
    • Display coordinates: Display location information on screen.
  • 最終的には下図のとおりになる

  • P519にAutoLayoutについて記載がある
    • やっぱし複雑だなあという印象。macOSでやっていたAutoLayOutとはまた違う。

CoreLocation

import CoreLocation
class CurrentLocationViewController: UIViewController, CLLocationManagerDelegate {

@IBAction func getLocation()

let authStatus = CLLocationManager.authorizationStatus()
if authStatus == .notDetermined {   // // CLの権限取得を未だ聞いていない場合
    locationManager.requestWhenInUseAuthorization()
    return
}
  • CLの権限取得。Alwaysもあるが(ナビゲーションアプリ用等)今回はWhen In Useを使う、ほとんどのアプリはこれ。

-w946

  • plistにRowを追加する、valueには文言を書く
// 権限の取得に失敗した場合
if authStatus == .denied || authStatus == .restricted {
    showLocationServicesDeniedAlert()
    return
}
  • 権限の取得に失敗した場合、ユーザへ知らせるためにshowLocationServicesDeniedAlert()を定義して呼び出している
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters   // 10メートルの精度
locationManager.startUpdatingLocation() // 計測スタート、結果はdelegateで取得する
  • 計測の設定と開始、結果の座標はdelegateで受け取る

-w682

  • 権限取得を拒否した場合は上記で変更する

locationManager(_ manager: CLLocationManager, didFailWithError error: Error)

  • 座標の取得に失敗した場合に呼ばれる

locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation])

  • 座標が更新されたとき
let newLocation = locations.last!
print("didUpdateLocations \(newLocation)")

location = newLocation  // 現在地をプロパティに保存
updateLabels()  // UIの更新
  • 現在地をプロパティに保存
  • その後UIを更新する

func showLocationServicesDeniedAlert()

  • 権限の取得に失敗したとき、ユーザへ分かりやすいようなポップアップを表示する
let alert = UIAlertController(title: "Location Services Disabled",
                              message: "Please enable locaiton services for this app in Settings",
                              preferredStyle: .alert)

let okAction = UIAlertAction(title: "OK",
                             style: .default,
                             handler: nil)
alert.addAction(okAction)

present(alert, animated: true, completion: nil)

updateLabels()

if let location = location {
    latitudeLabel.text  = String(format: ".%8f", location.coordinate.latitude)
    longitudeLabel.text = String(format: ".%8f", location.coordinate.longitude)
    tagButton.isHidden  = false
    messageLabel.text   = ""
} else {
    latitudeLabel.text  = ""
    longitudeLabel.text = ""
    addressLabel.text   = ""
    tagButton.isHidden  = true
    messageLabel.text   = "tap 'Get My Location' to Start"
}

iOS_Apprentice Chapter23

全般

  • 一度検索した後、シュミレータで位置を変更した場合、古い位置情報を保持してしまって、うまくSearching...と表示されず動かないことがある。
    • 再起動してやるといい

CurrentLocationViewController

Properties

コメントの通り

// CLLocation Manager Variables For Getting Coordinate
let locationManager = CLLocationManager() // GPS座標を与えるオブジェクト
var location: CLLocation?                 // 現在地の情報
var updatingLocation = false              // 座標の計測中かどうか
var lastLocationError: Error?

// Reverse Geocoding Variables For Getting Address
let geocoder = CLGeocoder()            // ReverseGeocodingを行うためのオブジェクト
var placemark: CLPlacemark?            // 取得した住所の情報
var performingReverseGeocoding = false // ReverseGeocogind中かどうか
var lastGeocodingError: Error?         // エラー情報を記録
    
// 1分経過したかの監視用
var timer: Timer?

// MARK:- CLLocationManagerDelegate

  • コメントの通り、深刻なエラーがあれば測定を中止する。
// MARK:- CLLocationManagerDelegate
// 座標の取得に失敗した場合
func locationManager(_ manager: CLLocationManager,
                     didFailWithError error: Error) {
    print("didFailWithError \(error.localizedDescription)")
    
    if (error as NSError).code == CLError.locationUnknown.rawValue {
        return  // 座標の取得に少し時間が必要な場合は続行    }
    lastLocationError = error   // 深刻なエラーであればプロパティに記録する
    stopLocationManager()
    updateLabels()
}
  • (P540)delegatecompletionHandlerの両方を用意しているAPIもある。
    • completionHandlerは同じ場所に書けるのでコンパクトに書ける利点がある。
// 座標がアップデートされたとき
func locationManager(_ manager: CLLocationManager,
                     didUpdateLocations locations: [CLLocation]) {
    let newLocation = locations.last!           // 最新の測定座標をピックアップ
    print("didUpdateLocations \(newLocation)")
    
    //////////////////////////
    // newLocationの有用性の判定
    //////////////////////////
    if newLocation.timestamp.timeIntervalSinceNow < -5 {
        return  // 5秒以上前のデータはキャッシュされているものなので無視する
    }
    if newLocation.horizontalAccuracy < 0 {
        return  // 負の値は、計測結果の無効を意味する
    }
    
    ///////////////////////////////////////
    // 最新の測定結果の分析とReverseGeocoding
    ///////////////////////////////////////
    // 座標の測定精度が良くなっているならば、その座標を採用する
    // →前に測定した座標と今回の座標の距離を計算する
    // 距離が初回計算の場合、つまりlocationがnilのとき、距離にDoubleの最大値を用いることになる
    var distance = CLLocationDistance(Double.greatestFiniteMagnitude)
    if let location = location {
        distance = newLocation.distance(from: location)
    }
    
    // 新しく受信したデータが、前のデータよりも正確なものかどうかをチェックする。
    // ||の前の条件がtrueであれば以降の条件は判定しない(= short circuiting)
    // 1番目の条件はつまり初回計測の場合
    // またaccuracyは値が小さいほど正確である。(10mのほうが100mよりも正確)
    if location == nil || location!.horizontalAccuracy > newLocation.horizontalAccuracy {
        // 新しい座標を採用する
        lastLocationError = nil // 座標が取得できたのでErrorを消去
        location = newLocation  // 現在地をプロパティに記録
        
        // 十分な精度で位置が取得できた場合、座標の測定を中止する
        if newLocation.horizontalAccuracy <= locationManager.desiredAccuracy {
            
            print("*** We're done!")
            stopLocationManager()
            
            if distance > 0 {
                // 前の計測結果が残っているために、測定精度が高いが距離が離れているような場合
                performingReverseGeocoding = false  // 再度ReverseGeocodingを行う
            }
        }
        updateLabels()
        
        // 座標から住所を取得する(Reverse Geocoding)
        if !performingReverseGeocoding {
            print("*** Going to geocode ***")
            
            performingReverseGeocoding = true
            
            geocoder.reverseGeocodeLocation(newLocation,
                                            completionHandler: {
                // 直ぐには行われず取得完了時にclosureの処理が行われる(CLLocationのdelegateとは異なっている)
                placemarks, error in
                self.lastGeocodingError = error // エラー情報の記録
                if error == nil, let p = placemarks, !p.isEmpty {
                    // エラー無く住所が見つかった場合
                    self.placemark = p.last!
                } else {
                    self.placemark = nil
                }
                self.performingReverseGeocoding = false
                self.updateLabels()
            })
        } else if distance < 1 {    // 現在ReverseGeocodingをしており、かつ距離が小さい場合
            let timeInterval = newLocation.timestamp.timeIntervalSince(location!.timestamp)
            
            // 1つ前の座標取得から10second以上経っているとき
            if timeInterval > 10 {
                print("*** Force done!")
                stopLocationManager()
                updateLabels()
            }
        }
    }
}

// MARK:- Helper Methods

  • ラベルの情報は、プロパティに保存された値を見てそれぞれ判断する。
// UI上のラベル情報を更新する
func updateLabels() {
    if let location = location {
        // 座標が計測できている場合
        
        latitudeLabel.text  = String(format: ".%8f", location.coordinate.latitude)
        longitudeLabel.text = String(format: ".%8f", location.coordinate.longitude)
        tagButton.isHidden  = false
        messageLabel.text   = ""
        
        // 住所の取得状況
        if let placemark = placemark {
            // 住所が取得できている場合
            addressLabel.text = string(from: placemark) // 住所オブジェクトから文字列を作成する
        } else if performingReverseGeocoding {
            // ReverseGeocoding中
            addressLabel.text = "Searcnihg for Address..."
        } else if lastGeocodingError != nil {
            // ReverseGeocodingでエラー
            addressLabel.text = "Error Finding Address"
        } else {
            // 住所が取得できていないがエラーも発生していない場合
            addressLabel.text = "No Address Found"
        }
    } else {
        // 座標が計測できていない場合
        latitudeLabel.text  = ""
        longitudeLabel.text = ""
        addressLabel.text   = ""
        tagButton.isHidden  = true
        let statusMessage: String
        
        if let error = lastLocationError as NSError? {
            // 座標の計測でエラーが発生している場合
            if error.domain == kCLErrorDomain && error.code == CLError.denied.rawValue {
                // CLLocationのErrorが発生している、エラーの内容はユーザが位置情報サービスの許可をしなかった場合。
                // (CLLocationManager.authorizationStatus()とは別なのだろうか?)
                statusMessage = "LocationServices Disabled"
            } else {
                // 座標を測定する方法がない場合等
                statusMessage = "Error Getting Location"
            }
        } else if !CLLocationManager.locationServicesEnabled() {
            // 座標の計測でエラーは記録されていないが、デバイス自体の位置情報サービス(CL)が使用不可の場合
            statusMessage = "Location Services Disabled"
        } else if updatingLocation {
            // 座標の計測でエラーは記録されておらず、座標を計測中の場合(最初の座標計測中)
            statusMessage = "Searching..."
        } else {
            // 座標をアップデートしていない場合
            statusMessage = "Tap 'Get My Location' to Start"
        }
        messageLabel.text = statusMessage
    }
    configureGetButton()
}
// 座標の計測を開始
func startLocationManager() {
    if CLLocationManager.locationServicesEnabled() {
        // デバイスの位置情報サービスの利用が可能な場合
        locationManager.delegate        = self                                // 測定結果はdelegateで取得する
        locationManager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters // 10メートルの測定精度とする
        locationManager.startUpdatingLocation()                               // 計測開始
        updatingLocation                = true
        timer = Timer.scheduledTimer(timeInterval : 60,   // タイマーのセット
                                     target       : self,
                                     selector     : #selector(didTimeOut),
                                     userInfo     : nil,
                                     repeats      : false)
    }
}
// 座標の計測を停止
func stopLocationManager() {
    if updatingLocation {
        // 座標の計測中の場合
        locationManager.stopUpdatingLocation()
        locationManager.delegate = nil
        updatingLocation         = false
        if let timer = timer {  // タイマを停止する
            timer.invalidate()
        }
    }
}
  • Propertyの情報でUIを切り替える
// ボタンのUIを変更する
func configureGetButton() {
    if updatingLocation {
        getButton.setTitle("Stop", for: .normal)
    } else {
        getButton.setTitle("Get My Location", for: .normal)
    }
}
// CLPlacemarkオブジェクトから住所の文字列を作成する
func string(from placemark: CLPlacemark) -> String {
    var line1 = ""
    
    if let s = placemark.subThoroughfare {
        line1 += s + " "
    }
    if let s = placemark.thoroughfare {
        line1 += s
    }
    
    var line2 = ""
    
    if let s = placemark.locality {
        line2 += s + " "
    }
    if let s = placemark.administrativeArea {
        line2 += s + " "
    }
    if let s = placemark.postalCode {
        line2 += s
    }
    
    return line1 + "\n" + line2
}
// 計測開始から1分以上経った場合に計測を中止する。セレクタとして呼ばれる
@objc func didTimeOut() {
    print("*** Time Out")
    if location == nil {
        stopLocationManager()
        lastLocationError = NSError(domain: "MyLocationsErrorDomain",
                                    code: 1,
                                    userInfo: nil)
        updateLabels()
    }
}

iOS_Apprentice Chapter24

P591

  • 構造体である
  • Array, Dictionaryも実際は構造体である
struct CLLocationCoordinate2D {
var latitude: CLLocationDegrees
var longitude: CLLocationDegrees
}
  • またCLLocationDegreesはDouble型。型名に意味を持たせている
typealias CLLocationDegrees = Double

iOS_Apprentice Chapter25

CurrentLocationViewController

  • コードに関してはコメントを参照
  • P582にLocationDetailViewのセルのAutoLayoutの設定
  • P603にunwindの設定

iOS_Apprentice Chapter26

trailing closure syntax

  • 以下のようなクロージャが最後の引数の関数の呼び出しについて
// free function(コードのどこからでも呼び出せる)
// @escaping (): すぐに実行しないclosureに必要
func afterDelay(_ seconds: Double,
                run: @escaping () -> Void) {
    // runで指定したclosureはdeadlineになったときに遅延実行される
    DispatchQueue.main.asyncAfter(deadline: .now() + seconds,
                                  execute: run)
}
  • 普通に呼び出す場合
afterDelay(0.6,
           run: {
            hudView.hide()  // Hudを隠す
            self.navigationController?.popViewController(animated: true)    // 現在の画面を閉じる
})
  • Swiftでは下記のようにさらに簡潔に書ける
// 画面を閉じる設定(0.6病後に画面を閉じる)
// 最後のパラメータがclosureであればそれを()から出すことができる(= trailing closure syntax 読みやすい。)
afterDelay(0.6) {
    hudView.hide()  // Hudを隠す
    self.navigationController?.popViewController(animated: true)    // 現在の画面を閉じる
}

iOS_Apprentice Chapter27

P633 NSManagedObjectについて

  • NSManagedObjectはCoreDataで管理される全てのオブジェクトの基底クラスである。
  • 通常はNSObjectからの継承だが、CoreDataオブジェクトはNSManagedObjectを継承している

  • CoreLocationからデータを復元する際、NSManagedObjectから通常のLocationクラスにXcodeが自動で変換している。

  • 今回はこの処理を自動で行う。

p637

The goal here is to create an NSManagedObjectContext object. That is the object you’ll use to talk to Core Data. To get that NSManagedObjectContext object, the app needs to do several things: 1. Create an NSManagedObjectModel from the Core Data model you created earlier. This object represents the data model during runtime. You can ask it what sort of entities it has, what attributes these entities have, and so on. In most apps, you don’t need to use the NSManagedObjectModel object directly. iOS Apprentice Chapter 27: Saving Locations raywenderlich.com 637 2. Create an NSPersistentStoreCoordinator object. This object is in charge of the SQLite database. 3. Finally, create the NSManagedObjectContext object and connect it to the persistent store coordinator.


But as of iOS 10, there is a new object, the NSPersistentContainer, that takes care of everything.

P640 dependency injection

  • クラスのプロパティから取得するのではなくて、NSManagedObjectを共有しよう、という構造。

-w686

P643

var persistentContainer: NSPersistentContainer
init() {
    persistentContainer = createPersistentContainer()
}
func createPersistentContainer() -> NSPersistentContainer {
    // all the initialization code here
    return container
}

上記のコードを、1つの宣言にまとめることができる

    // dataModelを読み込むのに必要なコード
    // ゴールはNSManagedObjectContextを作成することで、これはCoreDataと通信するためのものである
    lazy var persistentContainer: NSPersistentContainer = {
        // lazyとあるので、以下のclosureの中身は実際にオブジェクトが作成されるときに初めて実行される
        let container = NSPersistentContainer(name: "DataModel") // NSPersistentContainerオブジェクトの作成
        container.loadPersistentStores(completionHandler: {      // データベースからデータをロードし、CoreDataStackを用意する
            // データのロードが完了したときにclosureの中が呼ばれる
            storeDescription, error in
            if let error = error {
                fatalError("Could load data store: \(error)")    // エラーで終了する
            }
        })
        return container
    }()

iOS_Apprentice Chapter28

  • Tagではなくカスタムのサブクラスを使う優位性

Using a custom subclass for your table view cells, there is no limit to how complex the cell functionality can be.

  • lazyを使うのは良い習慣(P677)

It’s good to get into the habit of lazily loading objects. You don’t allocate them until you first use them. This makes your apps quicker to start and it saves memory.

  • NSFetchedResultsControllerとは、アップデートした情報を自動で更新してくれる便利なやつ

iOS_Apprentice Chapter29

  • guardを使うとearly returnでスッキリ書ける

-w717

iOS_Apprentice Chapter30

  • デバッグで条件式を操作したいときは下記のようにtrue ||をつける方法がある
if true || UIImagePickerController.isSourceTypeAvailable(.camera) {

CoreDataに保存するデータ型はObjective-Cで扱える必要がある(P730)

You may be wondering why you’re declaring the type of photoID as NSNumber and not as Int or, more precisely, Int32. Remember that Core Data is an Objective-C framework, so you’re limited by the possibilities of that language. NSNumber is how number objects are handled in Objective-C.

UITableViewの区切り線のすきま調整(P762)

        separatorInset = UIEdgeInsets(top: 0,   // セルの上下にある区切り線を左にずらして空白が無いにする
                                      left: 62,
                                      bottom: 0,
                                      right: 0)

AutoLayoutのデバッグ

-w381

// for debug
descriptionLabel.backgroundColor = UIColor.purple
addressLabel.backgroundColor = UIColor.purple

iOS_Apprentice Chapter31

下記ボタンを消去する際にCAAnimationDelegateを使用している

class CurrentLocationViewController: UIViewController, CLLocationManagerDelegate, CAAnimationDelegate {

-w392

再度ボタンを表示させるには

  • Noneにして再度計測→Stopにより表示される

Tip: To get the logo back so you can try again, first choose Location ▸ None from the Simulator’s Debug menu. Then tap Get My Location followed by Stop to make the logo reappear.

スピナーはStoryboardで作るほうが好きだけど、勉強のため今回はコードに書く(P774)

let spinner = UIActivityIndicatorView(style: .white)

ターミナルからCAF形式に変換する方法(P777)

If you want to use your own sound file but it is in a different format than CAF and your audio software can’t save CAF files, then you can use the afconvert utility to convert the audio file. You need to run it from the Terminal: $ /usr/bin/afconvert -f caff -d LEI16 Sound.wav Sound.caf