20200625のSwiftに関する記事は16件です。

SwiftでのJSONデータの取得とデータの取り出し方法

記事を書こうと思ったきっかけ

API通信をしてJSONデータを取得するときに様々なライブラリの使用が推奨されており、ごちゃごちゃになっていたのでまとめてみようと思いました。初心者でもわかりやすいように書いていきます。

API通信の手順

(今回はwebAPIに絞っていきます)
①HTTP通信をしてweb上のAPIにアクセス(今回はhttps://qiita.com/api/v2/itemsにアクセスします)
②HTTP通信をして受け取ったデータからコードに必要なデータをマッピングする(マッピングとはプログラミングで使いやすいようにデータを加工するという意味合いです。)

ライブラリについて

自分はここでこんがらがってしまったのですが上記の①と②に分けて整理して考えたら簡単です。概要だけ示して個々の詳しいやり方については後述していきます
①のやり方
1-1 URLSessionを使う
1-2 ライブラリであるAlamofireを使う
②のやり方
2-1 ライブラリであるSwiftyJSONを使う
2-2 Codableを使う

URLSessionを使う場合

①の手順についてはHTTP通信をするところまでを書きます。
まずは通信に必要なURLの準備をします。AlamofireでもURLSessionでもURL型のURLが必要なので、それを用意します。

    let urlString = "https://qiita.com/api/v2/items"
    let url = URL(string: urlString)

次に下記のようにコードを書き、実際に通信をします。

     let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
            if error != nil{
                print("Error")
            }
      }
      task.resume()

task.resume()を実行した時にHTTP通信が開始されます。URLSession.shared.dataTaskはそのために必要なオブジェクトを準備している認識です。URLSession.shared.dataTaskだけでは通信が開始されないので注意してください!
通信が終わったらクロージャ{(data.response,error) if ~~~}の部分が呼ばれます。(クロージャーというやつです。非同期処理とググってみると面白いですよ。簡単にいうと通信が終わってから実行されるということです。)
通信したときの結果は {(data.response,error) if ~~~}のdataやerrorに入っています。dataには通信した際のJSONデータがData型で入っており、もし通信をしている時にエラーが起こった場合はerrorにエラー内容が入ります。
以上がURLSessionを使ったHTTP通信です。

Alamofireを使う場合

URLの準備まではURLSessionを使う場合と同じです。
通信のコードは以下のようになっています

   AF.request(url!, method: .get, parameters: nil, encoding: JSONEncoding.default).responseJSON{(responce) in 
        if responce.result == failure {
            print("Error")
         }
    }

通信結果のデータはresponce.dataに入っています。通信が成功したかどうかはresponce.resultの値でわかります。successなら成功、failureなら失敗です。

自分はURLSessionをよく使っていたのでURLSessionの方がいいですが、resume()の書き忘れがなかったり、もっと複雑なAPI通信をした場合に、コードの可読性がマシになるという点から見るとAlamofireの方がいいのかもしれませんね。どちらも使いこなせるようになりたいものです。

SwiftyJSONを使う場合

今度はデータをマッピングしていく作業に入ります。
マッピングの際のコードはHTTP通信のコードのクロージャー内に書きます。そうすることで通信が終わったらデータが加工されるようになります。画面遷移などしたい場合もこのクロージャーの中に書くと非同期処理の遅れによるバグなどが回避できると思います。

まずはデータを扱いやすいように型を変更しましょう。

let json = JSON(data:data)

SwiftyJSONの機能であるJSON()でData型のデータをJSON型にしていきます。
URLSessionを使った場合は、JSON(data:data)を、Alamofireを使った場合はJSON(data.response.data)としてください。(dataやresponseという書き方は自分が習った書き方ですので、チーム内で統一されているのであれば適時それに読み替えてください。)

jsonのなかには以下のようなデータが入ってきます。(説明のためにかなり簡略化しています)

[
{title:"45秒で理解するMVC",created_at:"2020-06-25T21:37:15+09:00",user:{name:"たまや"}},
 {title:"1 コンストラクタの代わりにstaticファクトリメソッド",created_at:"2020-06-25T21:35:17+09:00",user:{name:""}}
]

配列で一つ一つのデータが並べられており、そのデータは辞書型で並べられています。
なのでもしtitleやuserのnameを取得したい場合は下記のように書くといいです

 for i in 1...10{
     let title = json![i]["title"]
     print(title)
     let userName = json![i]["user"]["name"]
     print(userName)
                    }

このようにすることで10件の投稿のタイトルと投稿者の氏名をゲットできます。つまりJSON型にしてしまえばあとは配列や辞書型と同じように扱えるというところがポイントですね。

Codableを使う場合

Codanleとは平たく言えば、あらかじめデータの構造にあった構造体を準備し、そこにデータを入れていくというものです。メリットとしては他のエンジニアが構造体を見ればAPIのデータがどのような構造をしているのかがわかる。構造体のプロパティ名とAPIのJSONデータのキー値が一致していないとエラーが起こり、かつネストされているデータは構造体内で同じようにネストしないとエラーが起こるのでコードの可読性が非常に高い。
なのでまずは構造体を準備しましょう.

struct Qiita:Codable {
    var id:String?
    var title:String?
    var updated_at:String?
    var user:User
    struct User:Codable{
        var name:String?
    }
}

JSONデータを見るとUserのところがネストされていたので構造体の中に構造体を作るという感じでネストしておきます。また構造体を定義する際にCodableと書くのを忘れずに。

次にデータを取り出しましょう。データを構造体の構造に変換する行為をデコードと言いますが、次ではデコードをしていくという認識で大丈夫です。これもクロージャーの中に書いてください。

let QiitaModel:[Qiita] = try! JSONDecoder().decode([Qiita].self, from: data!)
for i in 1...10{
   let title = QiitaModel[i].title
   print(title)
   let userName = QiitaModel[i].user.name

JSONDecorder().decode()を用いてデータをデコードしています。デコードしてデータをQiitaModelという配列に入れています。(デコードしたデータは配列で渡されるので配列を準備しています。)
その次にfor文の中でデータを取り出しています。今回は構造体のプロパティにアクセスしているため、QiitaModel[i].titleというような書き方をしています。QiitaModel[i]["title"]ではないので注意してください。

上記でもいった通りCodableには構造の制約がかかる分可読性が上がるといったメリットがありますし、データを取り出す際も簡単にできますので、自分はCodableの方が好きですね。

最後にまとめとしてURLSessionとCodableを併せて使ってみたコードを書こうと思います。

 import UIKit
 import SwiftyJSON
 import Alamofire

 struct Qiita:Codable {
     var id:String?
     var title:String?
     var updated_at:String?
     var user:User
     struct User:Codable{
         var name:String?
     }
 }

 class ViewController: UIViewController {

     override func viewDidLoad() {
         super.viewDidLoad()
         let urlString = "https://qiita.com/api/v2/items"
         let url = URL(string: urlString)
         let task = URLSession.shared.dataTask(with: url!) { (data, response, error) in
             if error != nil{
                 print(error)
             }else{

                 do{
                     print(data)
                     let QiitaModel:[Qiita] = try! JSONDecoder().decode([Qiita].self, from: data!)
                     for i in 1...10{
                        let title = QiitaModel[i].user.name
                        print(title)
                     }
                 }
             }
         }
         task.resume()
     }
 }

デコードの処理は関数の定義にthrowsと書いてありますので、エラー処理の中で記述しなければいけません。
エラー処理についての記事はこちらにわかりやすいものがあるので置いておきます。Swift 4.0 エラー処理入門
あとは上記でいったようにHTTP通信をし、そのクロージャーの中でデータを取り出すといった感じです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

iOS MultipeerConnectivity Frameworkに必要なデレゲートメソッドたち 【Swift】

はじめに

MCBrowserViewControllerを使わないパターンでのMultipeerConnectivity適用に必要なデレゲートメソッドたちです。

  • iOS 13.5
  • Swift 5.2

準備編

class xxxxxController {

    let displayName: String!
    let serviceType: String!
    var peerID: MCPeerID!
    var session: MCSession!
    var browser: MCNearbyServiceBrowser!
    var advertiser: MCNearbyServiceAdvertiser!

    func setup() {
        displayName = "displayName"
        serviceType = "serviceType"

        peerID = MCPeerID(displayName: displayName)
        session = MCSession(peer: peerID)
        session.delegate = self

        browser = MCNearbyServiceBrowser(peer: peerID, serviceType: serviceType)
        browser.delegate = self
        browser.startBrowsingForPeers()

        advertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)
        advertiser.delegate = self
        advertiser.startAdvertisingPeer()
    }
}

SessionDelegate編

extension xxxxxController: MCSessionDelegate {
    func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
    }

    func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
    }

    func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
    }

    func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
    }

    func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
    }    
}
sendメソッドたち
func send(_ data: Data, toPeers peerIDs: [MCPeerID], with mode: MCSessionSendDataMode) throws

func startStream(withName streamName: String, toPeer peerID: MCPeerID) throws -> OutputStream

func sendResource(at resourceURL: URL, withName resourceName: String, toPeer peerID: MCPeerID, withCompletionHandler completionHandler: ((Error?) -> Void)? = nil) -> Progress?

MCNearbyServiceBrowserDelegate編

extension xxxxxController: MCNearbyServiceBrowserDelegate {
    func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
    }

    func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: Error) {
    }

    func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
        browser.invitePeer(peerID, to: session, withContext: nil, timeout: 0)
    }

}

MCNearbyServiceAdvertiserDelegate編

extension xxxxxController: MCNearbyServiceAdvertiserDelegate {
    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
        invitationHandler(true, session)
        invitationHandler(false, nil)
    }

    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Error) {
    }
}

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

CreateMLでスポーツビデオのAction Classifierをつくる(書きかけ)

1、データセットをつくる。

動画に要求されること。

・各パターンごとに60動画(train50,test5,validation5)ぐらい必要。
・画面の中心に一人の全身がおさまっている。
・1クリップに1動作。前後の余分な動きはカットで収録されている。
・動画のフレームレート(1秒間に何コマか)は統一すべき。
・休憩や関係ない運動など、分類したい動作以外の「他の動作」動画も1分類として用意。

分類したい動作ごとにフォルダに入れ、フォルダの名前を動作名にする。

(このフォルダの名前が、分類結果の名前として出てくる)。

スクリーンショット 2020-06-25 20.00.29.png

スクリーンショット 2020-06-25 20.00.19.png

2、CreateMLでAction Clasifierモデルをトレーニングする。

XcodeのDeveloper ToolsからCreateMLをひらく。
スクリーンショット 2020-06-25 19.31.38.png

VideoセクションのAction Classificationを選択。
スクリーンショット 2020-06-25 19.32.49.png

Training Dataで、用意したデータセットフォルダを指定する。
スクリーンショット 2020-06-25 19.37.43.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift] map, flatMapの一般化された説明

はじめに

 SwiftにおけるmapflatMapについての記事はQiitaにもたくさんありますが、ArrayOptionalに限定されていて且つそれぞれ別々に説明されている記事が多いような印象を受けました(主観)。
 そこで、(自分が新しく作成した型についてmapで実装すべきかflatMapで実装すべきか迷った時に参照するためにも)この記事ではmapflatMapの特性について簡潔に 一般化された説明をしてみようという次第です。すごく詳細に一般化しようとすると「モナド」というものを理解しないといけないようですが、それは別の記事にお任せしましょう1
 既に同様の記事があったらごめんなさい。あと、この記事ではSwiftに限った話をしていますので悪しからず…。

総論

 mapflatMapも関数(クロージャ)を引数に取るいわゆる高階関数と呼ばれるものですが、元の型・引数となる関数(クロージャ)における返り値の型・高階関数の返り値の型に一定の関係性があり、その関係性によって区別がなされます。「二重が一重になるのがflat」みたいなのは一旦忘れてください。

各論

mapとは

動作

  • Hoge<T>の場合: (T) -> Uという関数(クロージャ)を引数にとって、Hoge<U>を返す。
  • Fuga<T1, T2>の場合: (T1) -> Uという関数(クロージャ)を引数にとって、Fuga<U, T2>を返す

即ち、mapは「引数となる関数(クロージャ)の返り値を同じ型で包み直す」ということです。

  • Array<Element>: func map<T>(_ transform: (Element) throws -> T) rethrows -> Array<T>
  • Optional<Wrapped>: func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> Optional<U>
  • Result<Success, Failure>: func map<NewSuccess>(_ transform: (Success) -> NewSuccess) -> Result<NewSuccess, Failure>

反例…?

  • String: func map<T>(_ transform: (Character) throws -> T) rethrows -> Array<T>
  • Dictionary: func map<T>(_ transform: ((key: Key, value: Value)) throws -> T) rethrows -> Array<T>

➡️これらはSequence由来なので、Sequecne<Element>において(Element) -> Tという関数(クロージャ)を引数にとってSequence<T>を返すと考えれば矛盾はありません。

flatMapとは

動作

  • Hoge<T>の場合: (T) -> Hoge<U>という関数(クロージャ)を引数にとって、Hoge<U>を返す。
  • Fuga<T1, T2>の場合: (T1) -> Fuga<U, T2>という関数(クロージャ)を引数にとって、Fuga<U, T2>を返す

即ち、「引数となる関数(クロージャ)の返り値とflatMap自身の返り値は型が同じ」ということです。

  • Optional<Wrapped>: func flatMap<U>(_ transform: (Wrapped) throws -> Optional<U>) rethrows -> Optional<U>
  • Result<Success, Failure>: func flatMap<NewSuccess>(_ transform: (Success) -> Result<NewSuccess, Failure>) -> Result<NewSuccess, Failure>

あれ?Arrayは…?

Array<Element>flatMapfunc flatMap<SegmentOfResult>(_ transform: (Element) throws -> SegmentOfResult) rethrows -> [SegmentOfResult.Element] where SegmentOfResult: Sequenceとなっています。なので、これはArrayに対するflatMapではなく、前述したようなStringと同様、Sequence<Element>に対するflatMapと捉えなければなりません。つまり、flatMapの引数となる関数(クロージャ)は(Element) -> Sequence<U>と考えることができ、そしてflatMapの返り値もSequence<U>と解釈できます。

注意

 昔のSwiftではflatMapという名前で現在のcompactMapと同じ動作が実装されていました。昔のドキュメントなどを参照して混乱なきよう…。
 ちなみにcompactMapSequence<Element>において.map(_:(Element) -> T?).filter({ $0 != nil })というような動作をします(正しいコードではなくあくまでイメージ)。

エピローグ

 Resultを実装するPR内でAppleの中の人flatMapについてコメントしていた内容にインスパイアされた記事でした。flatMapを「二重になったものを一重にするんだ」と思っていると、ResultにおけるflatMapが「??」となりそうだなと。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift5]Doubleの配列からh平均値を取得する

    func getAvarage(from array: [Double]) -> Double {
        let molecule = array.reduce(0, +)
        let denominator = Double(array.count)
        return molecule / denominator
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

NSAttributedStringをかんたんに生成する拡張関数(Swift)

はじめに

iOSアプリ開発では、複数行に渡る文章の行間( lineSpacing )など、文字列に複雑な装飾を施す場合、 NSAttributedString を生成する必要があります。
生成方法が複雑なので、引数を渡してかんたんに生成する拡張関数を実装しました。

環境

  • OS:macOS Mojave 10.14.6
  • Xcode:11.3.1 (11C504)
  • Swift:5.1.3

実装

イニシャライザを拡張関数として定義します。

NSAttributedString+Init.swift
import UIKit

extension NSAttributedString {
    convenience init(font: UIFont, color: UIColor, lineSpacing: CGFloat, alignment: NSTextAlignment, string: String) {
        var attributes: [NSAttributedString.Key: Any] = [
            .font: font,
            .foregroundColor: color
        ]
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.lineSpacing = lineSpacing
        paragraphStyle.alignment = alignment
        attributes.updateValue(paragraphStyle, forKey: .paragraphStyle)

        self.init(string: string, attributes: attributes)
    }
}

使い方

拡張関数として定義したイニシャライザを呼び出すのみです。

after
    @IBOutlet private weak var descriptionLabel: UILabel! {
        willSet {
            newValue.attributedText = NSAttributedString(
                font: .systemFont(ofSize: 16.0),
                color: .blue,
                lineSpacing: 6.0,
                alignment: .left,
                string: "description"
            )
        }
    }

拡張関数の定義前と比べると、スッキリ書けることがわかります。

before
    @IBOutlet private weak var descriptionLabel: UILabel! {
        willSet {
            var attributes: [NSAttributedString.Key: Any] = [
                .font: UIFont.systemFont(ofSize: 16.0),
                .foregroundColor: UIColor.blue
            ]
            let paragraphStyle = NSMutableParagraphStyle()
            paragraphStyle.lineSpacing = 6.0
            paragraphStyle.alignment = .left
            attributes.updateValue(paragraphStyle, forKey: .paragraphStyle)

            newValue.attributedText = NSAttributedString(string: "description", attributes: attributes)
        }
    }

おわりに

今回紹介した拡張関数は、私が必要としている最低限の引数のみ追加しているので、必要に応じてカスタマイズしてください。
NSMutableAttributedString として可変に扱う場合にもカスタマイズが必要です。

おまけ:他の方法

Twitterで他の人がどう NSAttributedString を生成しているか伺ったので、紹介します。

NSAttributedString を定数化する

行間の大きさが画面間で変わることも少ないので、 NSAttributedString ごと定数化するのはありだと思いました。
定数化の実装時に私が紹介した拡張関数を使うと、よりスッキリ書けると思います。

String から生成できるようにする

StringNSAttributedString を返す拡張関数を追加する方法です。
こちらもわかりやすくていいと思いました。
ただ文字列をローカライズしている場合、若干読みづらくなるかもしれません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift5】MapKitまとめ

MapKitに関する最新の情報が少ないように感じたので
よく使うような基本的な使い方についてまとめました!

今回の実装コードはGithubにあげているので自由にどうぞ!
Delegate部分はextentionで実装しているので多少異なります。
https://github.com/tomoki-inoue1221/mapkit-sample

網羅している内容

基本編

  • MapKitを使用するための設定と注意点
  • 現在地の表示
  • 現在地への照準を合わせる
  • 指定した場所にピンを立てる(タイトルやサブタイトルの設定)
  • ピンがタップされた時の処理
  • ロングタップでタップした場所にピンを立てる

応用編

  • 住所から緯度・経度を取得する(ジオコーディング)
  • 緯度・経度から住所を取得する(逆ジオコーディング)
  • カスタムピンの表示

基本編の実装

MapKit使用するための事前知識

現在地を表示する設定

現在地を表示するためには2点設定が必要です。

  1. Info.plistに許可設定用の内容を記述
  • Privacy - Location When In Use Usage Description
  • Privacy - Location Always Usage Description image.png
  1. storyBoardでMapViewのUserLocationにチェックを入れる image.png

現在地を表示する注意点

現在地を表示するためには実機での確認が必要です。

LocationManagerについて

Mapを使う上ではLocationManagerの存在は避けて通れないので、
こちらを一読しておくと良いです!
【CoreLocation】位置情報を取得する

よく使うDelegateメソッドの紹介

  • CLLocationManagerDelegate編
sample.swift
/// delegateをセットする呼ばれる(位置情報の許可設定に使用)
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {}

/// 自分の現在地が更新された時に呼ばれる(現在地更新した時に何か処理したい場合に使用)
/// locationsに現在地入ってる
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations:[CLLocation]) {}
  • MKMapViewDelegate編
sample.swift
/// ピンを追加した時に呼ばれる(ピンを加工したりする)
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {}

/// ピンをタップした時に呼ばれる(ピンの詳細情報を出したりする)
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {}

現在地の表示

位置情報の許可を求める

  • コードの実装
ViewController.swift
import UIKit
// 2つimportする
import MapKit
import CoreLocation

// CLLocationManagerDelegateを継承する
class ViewController: UIViewController, CLLocationManagerDelegate {
  // storyboardから接続する
  @IBOutlet weak var mapView: MKMapView!
  // locationManagerを宣言する
  var locationManager: CLLocationManager!

  override func viewDidLoad() {
     super.viewDidLoad()
     // ロケーションマネージャーのセットアップ
     locationManager = CLLocationManager()
     locationManager.delegate = self
     locationManager!.requestWhenInUseAuthorization()
  }

 // 許可を求めるためのdelegateメソッド
 func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        switch status {
        // 許可されてない場合
        case .notDetermined:
       // 許可を求める
            manager.requestWhenInUseAuthorization()
        // 拒否されてる場合
        case .restricted, .denied:
            // 何もしない
            break
        // 許可されている場合
        case .authorizedAlways, .authorizedWhenInUse:
            // 現在地の取得を開始
            manager.startUpdatingLocation()
            break
        default:
            break
        }
    }
}

こちらで、Info.plist・storyboardの設定がうまくできていれば
許可を求めるダイアログが表示されます。
S__7512083.jpg

許可を完了するとマップに現在地が表示されます。
S__7512084.jpg

現在地に照準を合わせる

  • コードの実装
ViewController.swift
import UIKit
import MapKit
import CoreLocation

class ViewController: UIViewController, CLLocationManagerDelegate {
  @IBOutlet weak var mapView: MKMapView!
  var locationManager: CLLocationManager!

  override func viewDidLoad() {
     super.viewDidLoad()
     // ロケーションマネージャーのセットアップ
     // 省略

   // 現在地に照準を合わす
     // 0.01が距離の倍率
     let span = MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
     // mapView.userLocation.coordinateで現在地の情報が取得できる
     let region = MKCoordinateRegion(center: mapView.userLocation.coordinate, span: span)
     // ここで照準を合わせている
     mapView.region = region
  }

}

初期の照準位置を変更したい場合は、
mapView.userLocation.coordinateを変更すればよくて、
例えば東京駅に合わせたければ、
東京駅の緯度・経度を調べて

ViewController.swift
// 省略

  override func viewDidLoad() {
     super.viewDidLoad()
     // ロケーションマネージャーのセットアップ
     // 省略

   // 東京駅に照準を合わす
     let span = MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
     // 東京駅の位置情報をセット
     let tokyoStation = CLLocationCoordinate2DMake(35.681236, 139.767125)
     // centerに東京駅のlocationDataをセット
     let region = MKCoordinateRegion(center: tokyoStation, span: span)
     mapView.region = region
  }

このように書くと東京駅に照準がアイマス。

指定した場所にピンを立てる

  • コードの実装
ViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()
        // ロケーションマネージャーのセットアップ

        // 現在地に照準を合わす


        // 指定値にピンを立てる
        // ピンを立てたい緯度・経度をセット
        // let coordinate = CLLocationCoordinate2DMake(35.45, 139.56)
        // 今回は現在地とする
        let coordinate = mapView.userLocation.coordinate
        // ピンを生成
        let pin = MKPointAnnotation()
     // ピンのタイトル・サブタイトルをセット
        pin.title = "タイトル"
        pin.subtitle = "サブタイトル"
        // ピンに一番上で作った位置情報をセット
        pin.coordinate = coordinate
        // mapにピンを表示する
        mapView.addAnnotation(pin)
   {

ピンがタップされた時の処理

ViewController.swift
// MKMapViewDelegateを継承
class ViewController: UIViewController,CLLocationManagerDelegate,MKMapViewDelegate {
    @IBOutlet weak var mapView: MKMapView!
    var locationManager: CLLocationManager!

    override func viewDidLoad() {
        super.viewDidLoad()
        // 省略

        // delegateをセット
        mapView.delegate = self
  }

    func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
        // タップされたピンの位置情報
        print(view.annotation?.coordinate)
        // タップされたピンのタイトルとサブタイトル
        print(view.annotation?.title)
        print(view.annotation?.subtitle)
    }

}

よくあるのはタップした時にモダール的な感じで、ピンの詳細がでてくるみたいな画面
GoogleMapで言うとこの井の頭自然文化公園の詳細情報みたいな感じを出すことが多いかも
S__7512085.jpg

ロングタップでタップした場所にピンを立てる(位置情報も取得)

  • コードの実装
ViewController.swift
    override func viewDidLoad() {
        super.viewDidLoad()

        //  省略

        // ロングタップを検知
        let longPress = UILongPressGestureRecognizer(target: self, action: #selector(recognizeLongPress(sender:)))
        //MapViewにリスナーを登録
        self.mapView.addGestureRecognizer(longPress)
    }

    //ロングタップした時に呼ばれる関数
    @objc func recognizeLongPress(sender: UILongPressGestureRecognizer) {
        //長押し感知は最初の1回のみ
        if sender.state != UIGestureRecognizer.State.began {
            return
        }

        // 位置情報を取得
        let location = sender.location(in: self.mapView)
        let coordinate = self.mapView.convert(location, toCoordinateFrom: self.mapView)
        // 出力
        print(coordinate.latitude)
        print(coordinate.longitude)
        // タップした位置に照準を合わせる処理
        let span = MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
        let region = MKCoordinateRegion(center: coordinate, span: span)
        self.mapView.region = region

        // ピンを生成
        let pin = MKPointAnnotation()
        pin.title = "タイトル"
        pin.subtitle = "サブタイトル"
        // タップした位置情報に位置にピンを追加
        pin.coordinate = coordinate
        self.mapView.addAnnotation(pin)
    }

ここで応用編でやる、逆ジオコーディングを使うと住所も取得できる。

応用編の実装

住所から緯度・経度を取得する(ジオコーディング)

よく使うのは住所検索した時に、検索位置に移動してピン立てるみたいな動き
※今回は簡略化のため、入力された想定で固定の住所から緯度・経度を取得する

ViewController.swift
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    // 画面に適当にボタンを配置する
    @IBAction func tap(_ sender: Any) {
        geoCording()
    }


    // ジオコーディング(住所から緯度・経度)
    func geoCording() {
        // 検索で入力した値を代入(今回は固定で東京駅)
        let address = "東京都千代田区丸の内1丁目"
        var resultlat: CLLocationDegrees!
        var resultlng: CLLocationDegrees!
        // 住所から位置情報に変換
        CLGeocoder().geocodeAddressString(address) { placemarks, error in
            if let lat = placemarks?.first?.location?.coordinate.latitude {
                // 問題なく変換できたら代入
                print("緯度 : \(lat)")
                resultlat = lat

            }
            if let lng = placemarks?.first?.location?.coordinate.longitude {
                // 問題なく変換できたら代入
                print("経度 : \(lng)")
                resultlng = lng
            }
            // 値が入ってれば
            if (resultlng != nil && resultlat != nil) {
          // 位置情報データを作成
                let cordinate = CLLocationCoordinate2DMake(resultlat, resultlng)
                let span = MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01)
          // 照準を合わせる
                let region = MKCoordinateRegion(center: cordinate, span: span)
                self.mapView.region = region

                // 同時に取得した位置にピンを立てる
                let pin = MKPointAnnotation()
                pin.title = "タイトル"
                pin.subtitle = "サブタイトル"

                pin.coordinate = cordinate
                self.mapView.addAnnotation(pin)
            }
        }
    }
}

これでボタンをタップした時に東京駅に照準があってピンが立つ

緯度・経度から住所を取得する(逆ジオコーディング)

よく使われるのは、基本編でやったロングタップした時に
そこの住所を出すみたいな動き

ViewController.swift
class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
    }

    // 画面に適当にボタンを配置する
    @IBAction func tap(_ sender: Any) {
        reverseGeoCording()
    }


    // 逆ジオコーデインング
    func reverseGeoCording() {
        // 住所を取得したい位置情報を宣言(今回は東京駅にセット)
        let location = CLLocation(latitude: 35.681236, longitude: 139.767125)
        // 位置情報から住所を取得
        CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in
            guard let placemark = placemarks?.first, error == nil else { return }
            // 市区町村より下の階層が出力
            print(placemark.name!)
            // 都道府県
            print(placemark.administrativeArea!)
            // なんとか郡とかがあれば(ない場合もあるのでnull回避)
            print(placemark.subAdministrativeArea ?? "")
            // 市区町村
            print(placemark.locality!)
            // これで日本語の住所はいい感じにでる
            print(placemark.administrativeArea! + placemark.locality! + placemark.name!)
        }
    }
}

placemarkにはいろんな値が入ってるので、
リファレンスはこちらの記事がわかりやすかったので参考に!
[iOS] MapKitを使って”ジオコーディング・逆ジオコーディング”をやってみる

カスタムピンの表示

よくある自分のオリジナルのピンを表示したりする感じです。
APIなどで地図に表示するデータを取得して表示する、みたいな時に使う感じになると思います。

ViewController.swift
import UIKit
import MapKit
import CoreLocation

// MKPointAnnotation(要するにピン)を継承したカスタムクラスを作成
class MapAnnotationSetting: MKPointAnnotation {
    // デフォルトだとピンにはタイトル・サブタイトルしかないので、設定を追加する
    // 今回は画像だけカスタムにしたいので画像だけ追加
    var pinImage: UIImage?
}



class ViewController: UIViewController,CLLocationManagerDelegate,MKMapViewDelegate {
    @IBOutlet weak var mapView: MKMapView!
    var locationManager: CLLocationManager!

    // とりあえずテストデータで画像・タイトル・サブタイトル・位置情報を用意
    let pinImagges: [UIImage?] = [UIImage(named: "inu1"),UIImage(named: "inu2")]
    let pinTitles: [String] = ["白いい犬","茶色い犬"]
    let pinSubTiiles: [String] = ["比較的白いです","茶色いのが売りです"]
    let pinlocations: [CLLocationCoordinate2D] = [CLLocationCoordinate2DMake(35.68, 139.56),CLLocationCoordinate2DMake(35.70, 139.56)]

    override func viewDidLoad() {
        super.viewDidLoad()

        // 省略

        // カスタムピンの表示
        // for文で配列の値を回す(ここはいろんなやり方があると思います。)
        for (index,pinTitle) in self.pinTitles.enumerated() {
            // カスタムで作成したMapAnnotationSettingをセット(これで画像をセットできる)
            let pin = MapAnnotationSetting()

            // 用意したデータをセット
            let coordinate = self.pinlocations[index]
            pin.title = pinTitle
            pin.subtitle = self.pinSubTiiles[index]
            // 画像をセットできる
            pin.pinImage = pinImagges[index]

            // ピンを立てる
            pin.coordinate = coordinate
            self.mapView.addAnnotation(pin)
        }
    }

   func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        // 自分の現在地は置き換えない(青いフワフワのマークのままにする)
        if (annotation is MKUserLocation) {
            return nil
        }

        let identifier = "pin"
        var annotationView: MKAnnotationView!

        if annotationView == nil {
            annotationView = MKAnnotationView(annotation: annotation, reuseIdentifier: identifier)
        }

        // ピンにセットした画像をつける
        if let pin = annotation as? MapAnnotationSetting {
            if let pinImage = pin.pinImage {
               annotationView.image = pinImage
            }
        }
        annotationView.annotation = annotation
        // ピンをタップした時の吹き出しの表示
        annotationView.canShowCallout = true

        return annotationView
    }


}

これで、カスタムのピンが表示される
S__7512086.jpg

Mapを触ってみて

いろんなことができすぎるのと、仕組みがまだ理解できていないのでちゃんと理解しないとしんどそう・・。
次はFirebaseと連携してUber的なアプリ作る

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】一度だけ実行したい処理の書き方

何回も呼ばれるメソッドで1度だけ行いたい処理の記述方法をまとめました。
やり方はたくさんありますが、これが1番私的には使いやすかったです。

1番簡単な方法

class OnceTrackerSample1 {
    var isFirst = Bool

    func manyCalledFunc() {
        if !isFirst { return }
        // 1回のみ行いたい処理 
    }
}

1番最初に全ての人間が思いつきそうな処理。
やっぱりダサいので、使い勝手よくカッコよく書きたい。

DispatchQueueを拡張して使いやすくする

DispatchQueueextensionします。

private static var _onceTracker = [String]()

class func once(token: String, block:()->Void) {
    // 排他制御
    objc_sync_enter(self); defer { objc_sync_exit(self) }
    // token: Stringで一意に実行状況を管理しています
    if _onceTracker.contains(token) { return }

    // 実行履歴を残して、block:()->Voidで渡された処理を実行
    _onceTracker.append(token)
    block()
}

使う側はこんな感じです。

class OnceTrackerSample1 {
    func manyCalledFunc() {
        DispatchQueue.once(token: "key name") {
            // 1回のみ行いたい処理 
        }
    }
}

スマートでいい感じ!
Stringkeyはどこかでまとめて管理したらより使いやすそう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

swiftメモ

コードから直接セーフアリアにconstraintする

swift5
import UIKit
import WebKit

class ViewController: UIViewController, WKNavigationDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()

        let webView = WKWebView(frame: .zero)
        webView.translatesAutoresizingMaskIntoConstraints = false
        webView.navigationDelegate = self
        view.addSubview(webView)

        webView.leftAnchor.constraint(equalTo: view.safeLeftAnchor).isActive = true
        webView.rightAnchor.constraint(equalTo: view.safeRightAnchor).isActive = true
        webView.topAnchor.constraint(equalTo: view.safeTopAnchor, constant: 60).isActive = true
        webView.bottomAnchor.constraint(equalTo: view.safeBottomAnchor, constant: 100).isActive = true
    }

}

配列を値から検索して削除

削除したい値以外で新しい配列を作成

swift5
        var array = ["a","b","c","d","e","f"]
        array = array.filter { $0 != "d" }
        print(array)    //結果:["a", "b", "c", "e", "f"]

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】 基本構文

変数の宣言

Variable.swift
 // 型も宣言する場合
 var 変数名: = 
 var age: Int = 20

 // 型推論が行われる場合
 var 変数名 = 
 var age = 20

 // 値を入れない(初期値nil)場合(オプショナル型)
 var 変数名: !
 var age: Int!

定数の宣言

Constant.swift
 // 型も宣言する場合
 let 変数名: = 
 let age: Int = 20

 // 型推論が行われる場合
 let 変数名 = 
 let age = 20

 // 値を入れない(初期値nil)場合(オプショナル型)
 let 変数名: !
 let age: Int!

メソッドの定義

Method.swift
 func メソッド名(第一引数名: , 第2引数名: ) -> 戻り値の型{
     return num1 + num2
 }

 func plus(num1: Int, num2: Int) -> Int {
    return num1 + num2
 }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UIButtonを画像付きのテキストにする

テキストの横に画像のあるボタンを作成したい場面に出会ったことのある人は結構いると思います。
そんな時UIImageUILabelを組み合わせなくてもUIButtonのみで作ることができます。

  • R.swiftを使っています
  • 表示する画像はこちらのイカちゃん iks.png

完成はこんな感じです。

UIButtonに画像とテキストを設定する

leftImageButton.setTitle("左側アイコン", for: .normal)
leftImageButton.setImage(R.image.ika(), for: .normal)

titleEdgeInsets・imageEdgeInsetsを使います

EdgeInsetsは余白の設定ができます。
それぞれ最適に余白の設定をすることで、ボタンとテキストをいい感じに表示させることができます。
初期値はtitleEdgeInsetsimageEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)なので、画像とテキストを同時に設定すると被って表示されます。

テキストの左に画像を付ける

テキストの左側に余白を設定します。画像のwidthを元に設定するのが良いと思います。

leftImageButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 0)

テキストの右に画像を付ける

画像を右側に持っていく為に左側にテキストの長さ分の余白を設定します。こちらも固定値を設定していますが、テキストの長さから設定した方が良いと思います。
またテキストの位置も調整したいので、テキストの余白も設定します。

rightImageButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 200, bottom: 0, right: 0)
rightImageButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 20)

仕組みがわかれば同じ要領でテキストの上側に画像を設定したり、下に画像を設定したりできます。

テキストの上に画像を付ける

topImageButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 100, bottom: 20, right: 0)
topImageButton.titleEdgeInsets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)

テキストの下に画像を付ける

bottomImageButton.imageEdgeInsets = UIEdgeInsets(top: 25, left: 100, bottom: 0, right: 0)
bottomImageButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0)

以上です。
仕組みを理解すれば簡単にちょっと凝ったボタンを作成することができてとても便利です。

関連

【Swift】UIButtonにimageをセットしたが表示されない件

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Swift】UIButtonを画像付きのテキストにする

テキストの横に画像のあるボタンを作成したい場面に出会ったことのある人は結構いると思います。
そんな時UIImageUILabelを組み合わせなくてもUIButtonのみで作ることができます。

  • R.swiftを使っています
  • 表示する画像はこちらのイカちゃん

iks.png

完成はこんな感じです。

UIButtonに画像とテキストを設定する

leftImageButton.setTitle("左側アイコン", for: .normal)
leftImageButton.setImage(R.image.ika(), for: .normal)

titleEdgeInsets・imageEdgeInsetsを使います

EdgeInsetsは余白の設定ができます。
それぞれ最適に余白の設定をすることで、ボタンとテキストをいい感じに表示させることができます。
初期値はtitleEdgeInsetsimageEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)なので、画像とテキストを同時に設定すると被って表示されます。

テキストの左に画像を付ける

テキストの左側に余白を設定します。画像のwidthを元に設定するのが良いと思います。

leftImageButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 0)

テキストの右に画像を付ける

画像を右側に持っていく為に左側にテキストの長さ分の余白を設定します。こちらも固定値を設定していますが、テキストの長さから設定した方が良いと思います。
またテキストの位置も調整したいので、テキストの余白も設定します。

rightImageButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 200, bottom: 0, right: 0)
rightImageButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 20)

仕組みがわかれば同じ要領でテキストの上側に画像を設定したり、下に画像を設定したりできます。

テキストの上に画像を付ける

topImageButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 100, bottom: 20, right: 0)
topImageButton.titleEdgeInsets = UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)

テキストの下に画像を付ける

bottomImageButton.imageEdgeInsets = UIEdgeInsets(top: 25, left: 100, bottom: 0, right: 0)
bottomImageButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0)

以上です。
仕組みを理解すれば簡単にちょっと凝ったボタンを作成することができてとても便利です。

関連

【Swift】UIButtonにimageをセットしたが表示されない件

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftUIでナビゲーションバーの中央にロゴ画像を配置する方法

最近SwiftUIを触り始めて、巷でよく見るNavigationBarの中央にロゴ画像を配置する実装をしようとしたところ、少しつまづいたので、備忘録がわりに書きます。

最終的にSwiftUIで以下の画像の様な状態を目指します。
スクリーンショット 2020-06-24 20.40.34.png

UIKit

まずは、UIKitでナビゲーションバーで中央に画像を配置する場合はUIViewControllerのnavigationItemが持つtitleViewというプロパティにUIImageViewを設定することで、表示することができます。

import UIKit

class ViewController: UIViewController {

    func viewDidLoad() {
        super.viewDidLoad()
        self.navigationItem.titleView = UIImageView(image: UIImage(named: "logo"))
    }
}

SwiftUI

SwiftUIで同じことをやろうとした場合、NavigationViewのタイトルの文字列を設定する関数は存在するのですが、どうやらUIKitでのtitleViewにあたる、ナビゲーションバーの中央にViewを配置する関数というのは存在しない様です。

import SwiftUI 

struct SwiftUIView: View {

    var body: some View {
        NavigationView {
            Text("Hello World")
                .navigationBarTitle("App Name", displayMode: .inline) 
                // 文字列は設定できるが、Imageは設定できない?
        }
    }
}

スクリーンショット 2020-06-24 21.02.33.png

調べてみるとナビゲーションバー左右にViewを配置する関数を利用して、画像を配置してから、画面のサイズを元に中央に寄せる方法などもあったのですが、元来想定されていなさそうな使い方の様に感じたので、別の方法で試してみました。

それが以下の方法です。

import SwiftUI

struct SwiftUIView: View {

    var body: some View {
        ZStack(alignment: .top) {
            NavigationView {
                Text("Hello, World!")
                    .navigationBarTitle("", displayMode: .inline)
            }
            Image(uiImage: UIImage(named: "logo"))
                .resizable()
                .scaledToFit()
                .frame(width: nil, height: 44, alignment: .center)
        }
    }
}

alignmentを.topに設定したZStack(Viewの重なりを設定するStack)に、NavigationViewとImageを配置することで、最前面の上部であるナビゲーションバーの中央にImageを表示する仕組みです。

この様にすると無事、UIKitと同様にNavigationBarの上部にロゴが表示されました
スクリーンショット 2020-06-24 21.12.40.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

EmbeddedFrameworkでCarthageを使用した際にPreviewがPotentialCrashError: Update failedになる場合の対処法

Xcode11から使用できる様になったPreview機能はSwiftUIでの開発をサポートしてくれる強力な味方ではあるのですが、特定の状況で読み込めなくなってしまうこともあり、そうなった場合は開発が滞ったりしてしまいます。

今回、EmbeddedFrameworkとCarthageを利用してSwiftUI開発している際に、Previewが読み込めなくなってハマった際の対処法を紹介します。

環境

  • Swift5.2
  • Xcode11.5
  • Carthage
    • Nuke(画像キャッシュ)
    • Alamofire(API通信)

プロジェクトの構成はアプリのターゲットに、DomainDataStorePresentationの三つのEmbeddedFrameworkを追加しています。
スクリーンショット 2020-06-25 0.21.05.png

UI周りのコードはPresentationに全て書いているので、そのFramework内でPreviewを確認します。

PotentialCrashError: Update failed

では、早速PresentationのFramework内に試しに書いたPreviewを見てみましょう。

struct SwiftUIView_Previews: PreviewProvider {

    static var previews: some View {
        Text("Hello, World!")
    }
}

スクリーンショット 2020-06-25 0.28.47.png

読み込めない?

エラーの詳細を見てみると・・・

スクリーンショット 2020-06-25 0.29.09.png

どうやらCarthageで導入しているNukeが読み込めないがためにPreviewが表示されないようです。

スクリーンショット 2020-06-25 0.37.05.png

しかしシミュレーターのビルドは通っているので、ちゃんとCarthageで導入したFrameworkの参照はうまくいってるはず?

解決法

色々と調べていると、PreviewからFrameworkの参照するためのパスの設定はBuild SettingのFramework Search PathsではなくRunpath Search Pathsから行われているようです。

スクリーンショット 2020-06-25 0.48.01.png

そうと分かれば、以下の画像のようにRunpath Search Pathに、新たにFramework Search Pathsに入力されている$(PROJECT_DIR)/Carthage/Build/iOSを追加してあげれば、Preview時にCarthageで導入したFrameworkも参照できるようになりそうです。

スクリーンショット 2020-06-25 0.51.33.png

この状態でPreviewを確認すると・・・

スクリーンショット 2020-06-25 0.56.24.png

ちゃんと表示された!?

最後に

以上がEmbeddedFrameworkにCarthageで導入したFramework際にPreviewが表示されない場合の対処法でした。

この記事によって、誰かが私のようにこの問題を修正するために、EmbeddedFramework化を諦めそうになったり、休日の大事な時間を無駄にしたりしないことを祈ります?

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Swift:あるNSWindowより下層領域のスクリーンショットを取得

retina画質に対応したものが取得できる。
NSImageの幅高さとCGImageの幅高さは異なることがあるので注意。

下準備

NSScreenとCGImageのExtensionを用意
import AppKit
import CoreGraphics

extension NSScreen {
    var displayID: CGDirectDisplayID {
        let key = NSDeviceDescriptionKey(rawValue: "NSScreenNumber")
        return deviceDescription[key] as! CGDirectDisplayID
    }
}

extension CGImage {
    static func background(_ displayID: CGDirectDisplayID,
                           _ windowID: CGWindowID,
                           _ frame: CGRect) -> CGImage? {
        let bounds = CGDisplayBounds(displayID)
        let windowOptions: CGWindowListOption = [
            .optionOnScreenOnly,
            .optionOnScreenBelowWindow
        ]
        let imageOptions: CGWindowImageOption = [
            .bestResolution,
            .boundsIgnoreFraming
        ]
        return CGWindowListCreateImage(bounds, windowOptions, windowID, imageOptions)
    }
}

画像を取得

class CustomWindow: NSWindow {

    func getScreenShotUnderThisWindow() -> NSImage? {
        let windowID = CGWindowID(windowNumber)
        guard let screen = self.screen,
            let cgImage = CGImage.background(screen.displayID, windowID, frame)
            else { return nil }
        return NSImage(cgImage: cgImage, size: frame.size)
    }

}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift5] Realmのよく使うところだけをサンプル付きでまとめてみた

Realm

概要

  • 保存できる型

    • Bool, Int, Int8, Int16, Int32, Int64, Double, Float, String, Date, Data.
  • CGFloatで保存することは推奨されない(できなくはないが型がRealmで保証されていない)

  • String,Date, DataOptional指定 が可能。
    その他の保存できる型で Optional指定 をしたい場合は、RealmOptionalを利用する。RealmOptionalは「Int, Float, Double, Bool」に対応。
    RealmOptionalプロパティは必ずletでなければいけない。

RealmDatabaseの作成(CREATE DATABASE)

// デフォルトのデータベース
let realm = try! Realm()

// ファイルを指定したデータベース
let realm = try! Realm(fileURL: YOUR_REALM_FILE_PATH)

// インメモリで使用するようにconfigを適用したデータベース
let configration = Realm.Configuration(inMemoryIdentifier: "メモリ空間名")
let realm = try! Realm(configration: configration) 

RealmDatabaseの削除(DROP DATABASE)

// データベースまるまる削除
NSFileManager.defaultManager().removeItemAtURL(Realm.Configuration.defaultConfiguration.fileURL!)

// ファイルは残すが、中身を空っぽに
try! realm.write {
  realm.deleteAll()
}

モデルの定義(CREATE TABLE)

import RealmSwift

// Dog model
class Dog: Object {
    @objc dynamic var name = ""
    @objc dynamic var owner: Person? // Properties can be optional
}

// Person model
class Person: Object {
    @objc dynamic var name = ""
    @objc dynamic var birthdate = Date(timeIntervalSince1970: 1)
    let dogs = List<Dog>()
}

モデルの継承

// Base Model
class Animal: Object {
    @objc dynamic var age = 0
}

// Models composed with Animal
class Duck: Object {
    @objc dynamic var animal: Animal? = nil
    @objc dynamic var name = ""
}
class Frog: Object {
    @objc dynamic var animal: Animal? = nil
    @objc dynamic var dateProp = Date()
}

// Usage
let duck = Duck(value: [
  "animal": [ 
    "age": 3
  ],
  "name": "Gustav"
    ]
)

モデルプロパティの属性

Realmモデルに定義されるプロパティは@obj dynamic var属性を持たなければならない。ただしSwift4以降で利用することのできる@objcMembers修飾子をクラスに宣言している場合、プロパティは単にdynamic var属性で宣言することができる。

/// Swift4 以前
// Dog model
class Dog: Object {
    @objc dynamic var name = ""
    @objc dynamic var owner: Person? // Properties can be optional
}

/// Swift4 以降
// Dog model
@objcMembers
class Dog: Object {
    dynamic var name = ""
    dynamic var owner: Person? // Properties can be optional
}

ただし、LinkingObjects, List, RealmOptional属性は動的なプロパティとして宣言することができないので、常にletで宣言しなければならない。

Type Non-optional Optional
Bool @objc dynamic var value = false let value = RealmOptional<Bool>()
Int @objc dynamic var value = 0 let value = RealmOptional<Int>()
Float @objc dynamic var value: Float = 0.0 let value = RealmOptional<Float>()
Double @objc dynamic var value: Double = 0.0 let value = RealmOptional<Double>()
String @objc dynamic var value = "" @objc dynamic var value: String? = nil
Data @objc dynamic var value = Data() @objc dynamic var value: Data? = nil
Date @objc dynamic var value = Date() @objc dynamic var value: Date? = nil
Object n/a @objc dynamic var value: Class?
List let value = List<Type>() n/a
LinkingObjects let value = LinkingObjects(fromType: Class.self, property: "property") n/a

プライマリキー

モデルを定義する時に primaryKeyメソッドをオーバーライドすることで、モデルの主キーを設定することができる。主キーを設定したオブジェクトをRealmに保存すると、後から変更することができない。

class Person: Object {
    @objc dynamic var id = 0
    @objc dynamic var name = ""

    override static func primaryKey() -> String? {
        return "id"
    }
}

インデックス

Realmでは各モデルでプロパティのインデックスをindexedPropertiesメソッドをオーバーライドすることで作成できる。インデックスを作成することで、等号演算、IN演算しを用いたクエリを高速に実行することができる。(※ インデックスを作成するため Realmファイルのサイズは大きくなる)

RealmではString, Int, Bool, Date型プロパティのインデックスに対応している。

class Book: Object {
    @objc dynamic var price = 0
    @objc dynamic var title = ""

    override static func indexedProperties() -> [String] {
        return ["title"]
    }
}

プロパティの無視

Realmに保存する必要のない変数(一時使用、一時的な変数)をignoredPropertiesメソッドをオーバーライドすることで設定できる。これを設定することでプロパティとして操作、利用することができるが、保存する際にデータは無視される。また、getterしか持たない変数は、自動的にモデル保存時に無視される。

class Person: Object {
    @objc dynamic var tmpID = 0
    var name: String { // read-only properties are automatically ignored
        return "\(firstName) \(lastName)"
    }
    @objc dynamic var firstName = ""
    @objc dynamic var lastName = ""

    override static func ignoredProperties() -> [String] {
        return ["tmpID"]
    }
}

モデルの書き込み

モデルを書き込むためには、モデルオブジェクトをインスタンス化してRealmに追加する必要がある。

class Dog: Object {
    @objc dynamic var name = ""
    @objc dynamic var age = 0
}

// (1) Create a Dog object and then set its properties
var myDog = Dog()
myDog.name = "Rex"
myDog.age = 10

// (2) Create a Dog object from a dictionary
let myOtherDog = Dog(value: ["name" : "Pluto", "age": 3])

// (3) Create a Dog object from an array
let myThirdDog = Dog(value: ["Fido", 5])
  1. オブジェクトのインスタンスを生成した後、直接プロパティに対して値を代入して作成する方法
  2. オブジェクトをインスタンス化する際にDictionaryを用いて、初期化して作成する方法
  3. オブジェクトをインスタンス化する際にArrayを用いて、初期化して作成する方法 (※ この場合、配列内要素の並びはモデルのプロパティ並びと同じでなければならない。またモデル保存時に無視されるプロパティやgetterのみのプロパティなどは無視する必要がある)

モデルオブジェクトを作成後、Realmに追加することができる。

// Get the default Realm
let realm = try! Realm()
// You only need to do this once (per thread)

// Add to the Realm inside a transaction
try! realm.write {
    realm.add(myDog)
}

Realmへの書き込みトランザクションが終了すると、他のスレッドでも追加した値を利用することができるようになる。このとき、複数の書き込みが同時進行で発生した場合は、他方の書き込みが終わるまでロックされる。これによって書き込みがロックされているスレッドもロックされることを注意する必要がある。

ただし、書き込みトランザクションが終了していない場合であっても、読み込みは可能である。

モデルの更新

モデル更新の方法は複数ある。

let author = ....
// Update an object with a transaction
try! realm.write {
    author.name = "Thomas Pynchon"
}
let persons = realm.objects(Person.self)
try! realm.write {
    persons.first?.setValue(true, forKey: "isFirst")
    // set each person's planet property to "Earth"
    persons.setValue("Earth", forKey: "planet")
}

プライマリキーを設定している場合は、プライマリキーによる更新を行うことができる。

// Creating a book with the same primary key as a previously saved book
let cheeseBook = Book()
cheeseBook.title = "Cheese recipes"
cheeseBook.price = 9000
cheeseBook.id = 1

// Updating book with id = 1
try! realm.write {
    realm.add(cheeseBook, update: .modified)
}

この時、主キー(id: 1)がすでに存在していれば単に更新処理が走る。主キーが存在しない場合は create と同等の処理が実行される。また、主キーと更新したい値のサブセットを引数にすることで差分更新を行うことができる。

// Assuming a "Book" with a primary key of `1` already exists.
try! realm.write {
    realm.create(Book.self, value: ["id": 1, "price": 9000.0], update: .modified)
    // the book's `title` property will remain unchanged.
}

ただし、主キーを定義していないモデルオブジェクトを更新する場合には update: の引数に .modified, .all を渡すことができない。また、.modified, .allを使った多重(コンフリクトが発生するような)書き込みの結果には注意が必要。

/// current DB recode
Book: ["id": 1, title: "Cheese recipes" "price": 9000.0]

/// create some write transactions ( cause conflict )
Book: ["id": 1, title: "Fruit recipes", price: 9000], update: .all
Book: ["id": 1, title: "Cheese recipes", price: 4000], update: .all

この場合、2つの更新が同時に発生するためコンフリクトが発生する。これらの書き込みがマージされた結果として得られる結果は、以下のどちらかとなる。

Book: ["id": 1, title: "Fruit recipes", price: 9000]
Book: ["id": 1, title: "Cheese recipes", price: 4000]

しかしupdate: .modifiedとした場合は、挙動が変わる。

/// current DB recode
Book: ["id": 1, title: "No.4" "price": 9000.0]

/// create some write transactions ( cause conflict )
Book: ["id": 1, title: "Fruit recipes", price: 9000], update: .modified
Book: ["id": 1, title: "Cheese recipes", price: 4000], update: .modified

この場合の書き込みがマージされた結果は以下のようになる

Book: ["id": 1, title: "Fruit recipes", price: 4000]

.all は完全置き換え、.modified は差分置き換えであることを注意する必要がある。

モデルの削除

書き込みトランザクションの中で以下を実行する。

// let cheeseBook = ... Book stored in Realm

// Delete an object with a transaction
try! realm.write {
    realm.delete(cheeseBook)
}

// Delete all objects from the realm
try! realm.write {
    realm.deleteAll()
}

モデルの変更通知

Realmのオブジェクトインスタンスは自動的に更新される。つまり、オブジェクトのプロパティを変更すると、同じオブジェクトを参照している他のインスタンスに、その変更が即座に反映される。Realmオブジェクトの更新通知を購読することで、対象のRealmオブジェクトに変更があった場合に通知を取得できる。

/// Model
class StepCounter: Object {
    @objc dynamic var steps = 0
}

/// Realm Object
let stepCounter = StepCounter()
let realm = try! Realm()
try! realm.write {
    realm.add(stepCounter)
}

/// NotificationToken
var token : NotificationToken?
token = stepCounter.observe { change in
    switch change {

      /// Realm Object is change
        case .change(let properties):
        for property in properties {
            if property.name == "steps" && property.newValue as! Int > 1000 {
            print("Congratulations, you've exceeded 1000 steps.")
            token = nil
          }
        }

      /// Realm Object cause error
        case .error(let error):
        print("An error occurred: \(error)")

      /// Realm Object was deleted
        case .deleted:
        print("The object was deleted.")
    }
}

モデル間のリレーション

  • 1:N, N:1
// Dog model
class Dog: Object {
    @objc dynamic var name = ""
    @objc dynamic var owner: Person? // Properties can be optional
}

// Person model
class Person: Object {
    @objc dynamic var name = ""
    @objc dynamic var birthdate = Date(timeIntervalSince1970: 1)
    let dogs = List<Dog>()
}

/// to-one relation
let jim = Person()
let rex = Dog()
rex.owner = jim

/// Many-to-one
let someDogs = realm.objects(Dog.self).filter("name contains 'Fido'")
jim.dogs.append(objectsIn: someDogs)
jim.dogs.append(rex)
  • 双方向リレーション
    一般的にモデル同士のリレーションはリンクされてる側からインスタンスを参照することはできるが、逆方向からは辿ることができない。RealmではLinkingObjectsを用いることで、双方向リレーションを形成することができる。LinkingObjectsは特定のプロパティ(ここではPersonモデルのdogsプロパティ)から与えられたオブジェクトにリンクされているすべてのオブジェクトを取得することができる。
class Dog: Object {
    @objc dynamic var name = ""
    @objc dynamic var age = 0
    let owners = LinkingObjects(fromType: Person.self, property: "dogs")
}

Appendix

JSONによるRecord作成

Realm は直接JSONをサポートしていないが、JSONSerialization の出力を用いでオブジェクトを生成することができる。JSON内にネストされてるオブジェクトや配列は、自動的にリレーションにマッピングされることに注意する必要がある。

この方法でオブジェクトを生成するときには、いくつかの注意点が存在する。

  • オブジェクトのプロパティ名とJSONのキー名が一致して、型が同じである必要があります。

  • floatプロパティは、floatをバックにしたNSNumbersで初期化する必要があります。

  • DateプロパティとDataプロパティは文字列から自動的に推論することはできませんが、Realm().create(_:value:update:)に渡す前に適切な型に変換しなければなりません。

  • 必須プロパティに JSON NULL (すなわち NSNull) が与えられた場合、例外がスローされます。

  • insert 時に必須のプロパティが与えられない場合は、例外がスローされます。

  • Realmは、Objectで定義されていないJSON内のプロパティを無視します。

// A Realm Object that represents a city
class City: Object {
    @objc dynamic var name = ""
    @objc dynamic var cityId = 0
    // other properties left out ...
}

let data = "{\"name\": \"San Francisco\", \"cityId\": 123}".data(using: .utf8)!
let realm = try! Realm()

// Insert from Data containing JSON
try! realm.write {
    let json = try! JSONSerialization.jsonObject(with: data, options: [])
    realm.create(City.self, value: json, update: .modified)
}

モデルオブジェクト更新時の設定

Realm では Realm に書き込まれたか否かで、更新方法が変わる場合がある。

  • アンマネージドオブジェクト
    Realm に書き込まれる前のオブジェクト( realm.write クロージャーの外でプロパティを変更できる)
  • マネージドオブジェクト
    Realm に書き込まれた後のオブジェクト (realm.write クロージャーの外でプロパティを変更できない)
@IBAction func tappedButton(_ sender: Any) {

  /*
   * この時点のpersonはRealmに書き込まれていないのでアンマネージドオブジェクト
   */
  let person = Person(value: ["name": "Yu", "age": 32])

  // 書き込まれていないので、realm.write内でなくてもプロパティの更新は問題なし
  person.mood = "Happy" 

  do {
      let realm = try Realm()
      try! realm.write {
        realm.add(person) // PersonをRealmに保存したことで、マネージドオブジェクトになる
        print("1回目成功だよ", person)
      }
  } catch {
      print("エラーだよ")
  }

  /*
   * この時点のPersonはマネージドオブジェクトなので、プロパティの更新はrealm.write内のみ可能
   */

  // person.mood = "sad" // 例外が発生する
  do {
      let realm = try Realm()
      try! realm.write {
          person.mood = "Sad" // プロパティの更新
          print("2回目成功だよ", person)
      }
  } catch {
      print("エラーだよ")
  }
}

一般的には、このように分離して処理を変更するべきであるが、以下のようにすることもできる。

@IBAction func tappedButton(_ sender: Any) {

  /*
   * この時点のpersonはRealmに書き込まれていないのでアンマネージドオブジェクト
   */
  let person = Person(value: ["name": "Yu", "age": 32])

  // 書き込まれていないので、realm.write内でなくてもプロパティの更新は問題なし
  person.mood = "Happy" 

  do {
      let realm = try Realm()
      try! realm.write {
        realm.add(person, updated: .modified) // PersonをRealmに保存したことで、マネージドオブジェクトになる
        print("1回目成功だよ", person)
      }
  } catch {
      print("エラーだよ")
  }

  /*
   * この時点のPersonはマネージドオブジェクトなので、プロパティの更新はrealm.write内のみ可能
   */

  // person.mood = "sad" // 例外が発生する
  do {
      let realm = try Realm()
      try! realm.write {
          realm.add(person, updated: .modified) "Sad" // プロパティの更新
          print("2回目成功だよ", person)
      }
  } catch {
      print("エラーだよ")
  }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む