- 投稿日:2020-06-25T22:47:20+09:00
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.nameJSONDecorder().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通信をし、そのクロージャーの中でデータを取り出すといった感じです。
- 投稿日:2020-06-25T20:07:41+09:00
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) { } }以上
- 投稿日:2020-06-25T19:10:02+09:00
CreateMLでスポーツビデオのAction Classifierをつくる(書きかけ)
1、データセットをつくる。
動画に要求されること。
・各パターンごとに60動画(train50,test5,validation5)ぐらい必要。
・画面の中心に一人の全身がおさまっている。
・1クリップに1動作。前後の余分な動きはカットで収録されている。
・動画のフレームレート(1秒間に何コマか)は統一すべき。
・休憩や関係ない運動など、分類したい動作以外の「他の動作」動画も1分類として用意。分類したい動作ごとにフォルダに入れ、フォルダの名前を動作名にする。
(このフォルダの名前が、分類結果の名前として出てくる)。
2、CreateMLでAction Clasifierモデルをトレーニングする。
XcodeのDeveloper ToolsからCreateMLをひらく。
- 投稿日:2020-06-25T17:53:18+09:00
[Swift] map, flatMapの一般化された説明
はじめに
Swiftにおける
map
やflatMap
についての記事はQiitaにもたくさんありますが、Array
とOptional
に限定されていて且つそれぞれ別々に説明されている記事が多いような印象を受けました(主観)。
そこで、(自分が新しく作成した型についてmap
で実装すべきかflatMap
で実装すべきか迷った時に参照するためにも)この記事ではmap
とflatMap
の特性について簡潔に 一般化された説明をしてみようという次第です。すごく詳細に一般化しようとすると「モナド」というものを理解しないといけないようですが、それは別の記事にお任せしましょう1。
既に同様の記事があったらごめんなさい。あと、この記事ではSwiftに限った話をしていますので悪しからず…。総論
map
もflatMap
も関数(クロージャ)を引数に取るいわゆる高階関数と呼ばれるものですが、元の型・引数となる関数(クロージャ)における返り値の型・高階関数の返り値の型に一定の関係性があり、その関係性によって区別がなされます。「二重が一重になるのが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>
でflatMap
はfunc 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
と同じ動作が実装されていました。昔のドキュメントなどを参照して混乱なきよう…。
ちなみにcompactMap
はSequence<Element>
において.map(_:(Element) -> T?).filter({ $0 != nil })
というような動作をします(正しいコードではなくあくまでイメージ)。エピローグ
Result
を実装するPR内でAppleの中の人がflatMap
についてコメントしていた内容にインスパイアされた記事でした。flatMap
を「二重になったものを一重にするんだ」と思っていると、Result
におけるflatMap
が「??」となりそうだなと。
たとえば、「モナドについてSwiftで説明してみた」。 ↩
- 投稿日:2020-06-25T17:25:26+09:00
[Swift5]Doubleの配列からh平均値を取得する
func getAvarage(from array: [Double]) -> Double { let molecule = array.reduce(0, +) let denominator = Double(array.count) return molecule / denominator }
- 投稿日:2020-06-25T17:00:16+09:00
NSAttributedStringをかんたんに生成する拡張関数(Swift)
はじめに
iOSアプリ開発では、複数行に渡る文章の行間(
lineSpacing
)など、文字列に複雑な装飾を施す場合、NSAttributedString
を生成する必要があります。
生成方法が複雑なので、引数を渡してかんたんに生成する拡張関数を実装しました。環境
- OS:macOS Mojave 10.14.6
- Xcode:11.3.1 (11C504)
- Swift:5.1.3
実装
イニシャライザを拡張関数として定義します。
NSAttributedString+Init.swiftimport 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
を定数化するよくアプリ内で使うAttributeを定数化ですね...
— 大渕雄生@未踏2020! (@obuchi_yuki) June 25, 2020
enum Attributeという名前空間作ってその中に static で書いていっちゃう感じです。行間の大きさが画面間で変わることも少ないので、
NSAttributedString
ごと定数化するのはありだと思いました。
定数化の実装時に私が紹介した拡張関数を使うと、よりスッキリ書けると思います。
String
から生成できるようにする自分は極力楽したくて
— もちゅる|【転職活動中】フルリモートパパエンジニア (@mothule) June 25, 2020
"文字列".decode([DecoratableType])
で書けるlibを書いた https://t.co/z3gngLRzcJ
String
にNSAttributedString
を返す拡張関数を追加する方法です。
こちらもわかりやすくていいと思いました。
ただ文字列をローカライズしている場合、若干読みづらくなるかもしれません。
- 投稿日:2020-06-25T16:48:59+09:00
【Swift5】MapKitまとめ
MapKitに関する最新の情報が少ないように感じたので
よく使うような基本的な使い方についてまとめました!今回の実装コードはGithubにあげているので自由にどうぞ!
Delegate部分はextentionで実装しているので多少異なります。
https://github.com/tomoki-inoue1221/mapkit-sample網羅している内容
基本編
- MapKitを使用するための設定と注意点
- 現在地の表示
- 現在地への照準を合わせる
- 指定した場所にピンを立てる(タイトルやサブタイトルの設定)
- ピンがタップされた時の処理
- ロングタップでタップした場所にピンを立てる
応用編
- 住所から緯度・経度を取得する(ジオコーディング)
- 緯度・経度から住所を取得する(逆ジオコーディング)
- カスタムピンの表示
基本編の実装
MapKit使用するための事前知識
現在地を表示する設定
現在地を表示するためには2点設定が必要です。
- Info.plistに許可設定用の内容を記述
現在地を表示する注意点
現在地を表示するためには実機での確認が必要です。
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.swiftimport 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の設定がうまくできていれば
許可を求めるダイアログが表示されます。
現在地に照準を合わせる
- コードの実装
ViewController.swiftimport 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.swiftoverride 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で言うとこの井の頭自然文化公園の詳細情報みたいな感じを出すことが多いかも
ロングタップでタップした場所にピンを立てる(位置情報も取得)
- コードの実装
ViewController.swiftoverride 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.swiftclass 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.swiftclass 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.swiftimport 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 } }Mapを触ってみて
いろんなことができすぎるのと、仕組みがまだ理解できていないのでちゃんと理解しないとしんどそう・・。
次はFirebaseと連携してUber的なアプリ作る
- 投稿日:2020-06-25T14:04:31+09:00
【Swift】一度だけ実行したい処理の書き方
何回も呼ばれるメソッドで1度だけ行いたい処理の記述方法をまとめました。
やり方はたくさんありますが、これが1番私的には使いやすかったです。1番簡単な方法
class OnceTrackerSample1 { var isFirst = Bool func manyCalledFunc() { if !isFirst { return } // 1回のみ行いたい処理 } }1番最初に全ての人間が思いつきそうな処理。
やっぱりダサいので、使い勝手よくカッコよく書きたい。DispatchQueueを拡張して使いやすくする
DispatchQueue
をextension
します。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回のみ行いたい処理 } } }スマートでいい感じ!
String
のkey
はどこかでまとめて管理したらより使いやすそう。
- 投稿日:2020-06-25T11:10:41+09:00
swiftメモ
コードから直接セーフアリアにconstraintする
swift5import 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 } }配列を値から検索して削除
削除したい値以外で新しい配列を作成
swift5var array = ["a","b","c","d","e","f"] array = array.filter { $0 != "d" } print(array) //結果:["a", "b", "c", "e", "f"]
- 投稿日:2020-06-25T09:14:17+09:00
【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.swiftfunc メソッド名(第一引数名: 型, 第2引数名: 型) -> 戻り値の型{ return num1 + num2 } func plus(num1: Int, num2: Int) -> Int { return num1 + num2 }
- 投稿日:2020-06-25T08:49:06+09:00
UIButtonを画像付きのテキストにする
テキストの横に画像のあるボタンを作成したい場面に出会ったことのある人は結構いると思います。
そんな時UIImage
とUILabel
を組み合わせなくてもUIButton
のみで作ることができます。完成はこんな感じです。
UIButtonに画像とテキストを設定する
leftImageButton.setTitle("左側アイコン", for: .normal) leftImageButton.setImage(R.image.ika(), for: .normal)titleEdgeInsets・imageEdgeInsetsを使います
EdgeInsets
は余白の設定ができます。
それぞれ最適に余白の設定をすることで、ボタンとテキストをいい感じに表示させることができます。
初期値はtitleEdgeInsets
もimageEdgeInsets
も(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)以上です。
仕組みを理解すれば簡単にちょっと凝ったボタンを作成することができてとても便利です。関連
- 投稿日:2020-06-25T08:49:06+09:00
【Swift】UIButtonを画像付きのテキストにする
テキストの横に画像のあるボタンを作成したい場面に出会ったことのある人は結構いると思います。
そんな時UIImage
とUILabel
を組み合わせなくてもUIButton
のみで作ることができます。
- R.swiftを使っています
- 表示する画像はこちらのイカちゃん
完成はこんな感じです。
UIButtonに画像とテキストを設定する
leftImageButton.setTitle("左側アイコン", for: .normal) leftImageButton.setImage(R.image.ika(), for: .normal)titleEdgeInsets・imageEdgeInsetsを使います
EdgeInsets
は余白の設定ができます。
それぞれ最適に余白の設定をすることで、ボタンとテキストをいい感じに表示させることができます。
初期値はtitleEdgeInsets
もimageEdgeInsets
も(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)以上です。
仕組みを理解すれば簡単にちょっと凝ったボタンを作成することができてとても便利です。関連
- 投稿日:2020-06-25T07:40:51+09:00
SwiftUIでナビゲーションバーの中央にロゴ画像を配置する方法
最近SwiftUIを触り始めて、巷でよく見るNavigationBarの中央にロゴ画像を配置する実装をしようとしたところ、少しつまづいたので、備忘録がわりに書きます。
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は設定できない? } } }調べてみるとナビゲーションバー左右に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を表示する仕組みです。
- 投稿日:2020-06-25T07:39:38+09:00
EmbeddedFrameworkでCarthageを使用した際にPreviewがPotentialCrashError: Update failedになる場合の対処法
Xcode11から使用できる様になったPreview機能はSwiftUIでの開発をサポートしてくれる強力な味方ではあるのですが、特定の状況で読み込めなくなってしまうこともあり、そうなった場合は開発が滞ったりしてしまいます。
今回、EmbeddedFrameworkとCarthageを利用してSwiftUI開発している際に、Previewが読み込めなくなってハマった際の対処法を紹介します。
環境
- Swift5.2
- Xcode11.5
- Carthage
- Nuke(画像キャッシュ)
- Alamofire(API通信)
プロジェクトの構成はアプリのターゲットに、
Domain
、DataStore
、Presentation
の三つのEmbeddedFrameworkを追加しています。
UI周りのコードは
Presentation
に全て書いているので、そのFramework内でPreviewを確認します。PotentialCrashError: Update failed
では、早速PresentationのFramework内に試しに書いたPreviewを見てみましょう。
struct SwiftUIView_Previews: PreviewProvider { static var previews: some View { Text("Hello, World!") } }読み込めない?
エラーの詳細を見てみると・・・
どうやらCarthageで導入しているNukeが読み込めないがためにPreviewが表示されないようです。
しかしシミュレーターのビルドは通っているので、ちゃんとCarthageで導入したFrameworkの参照はうまくいってるはず?
解決法
色々と調べていると、PreviewからFrameworkの参照するためのパスの設定はBuild Settingの
Framework Search Paths
ではなくRunpath Search Paths
から行われているようです。そうと分かれば、以下の画像のように
Runpath Search Path
に、新たにFramework Search Paths
に入力されている$(PROJECT_DIR)/Carthage/Build/iOS
を追加してあげれば、Preview時にCarthageで導入したFrameworkも参照できるようになりそうです。この状態でPreviewを確認すると・・・
ちゃんと表示された!?
最後に
以上がEmbeddedFrameworkにCarthageで導入したFramework際にPreviewが表示されない場合の対処法でした。
この記事によって、誰かが私のようにこの問題を修正するために、EmbeddedFramework化を諦めそうになったり、休日の大事な時間を無駄にしたりしないことを祈ります?
- 投稿日:2020-06-25T03:15:56+09:00
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) } }
- 投稿日:2020-06-25T00:12:55+09:00
[Swift5] Realmのよく使うところだけをサンプル付きでまとめてみた
Realm
概要
保存できる型
Bool
,Int
,Int8
,Int16
,Int32
,Int64
,Double
,Float
,String
,Date
,Data
.
CGFloat
で保存することは推奨されない(できなくはないが型がRealmで保証されていない)
String
,Date
,Data
はOptional
指定 が可能。
その他の保存できる型で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])
- オブジェクトのインスタンスを生成した後、直接プロパティに対して値を代入して作成する方法
- オブジェクトをインスタンス化する際に
Dictionary
を用いて、初期化して作成する方法- オブジェクトをインスタンス化する際に
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("エラーだよ") } }