はじめに
パッケージマネージャ編の続きです。
最近よく使ってるライブラリの紹介です。基本的にこいつらは標準装備でいいんじゃないかなと思ってます。
- mac-cain13/R.swift
- realm/SwiftLint
- mono0926/LicensePlis
- ishkawa/DIKit
- realm/realm-cocoa
- ishkawa/APIKit
- SwiftyBeaver/SwiftyBeaver
- Quick/Quick
- Quick/Nimble
- Firebase/Analytics
プロジェクトのフォルダ構成は下記でやってます。
R.swift (5.2.2)
mac-cain13/R.swift リソース管理がいい感じになる標準で使うべきやつ!!(似たようなので SwiftGen/SwiftGen ってのもあるみたいです)
- Pods でインストール
- Run Script 追可
- Run Script に記載
if which $PODS_ROOT/R.swift/rswift >/dev/null; then
"$PODS_ROOT/R.swift/rswift" generate "$SRCROOT/$PRODUCT_NAME/Resource/R.generated.swift"
else
echo "warning: R.swift not installed, download from https://github.com/mac-cain13/R.swift" - Input Files に
$TEMP_DIR/rswift-lastrun
を追加 - Outout Files に
$SRCROOT/$PRODUCT_NAME/Resource/R.generated.swift
を追加 - ビルドして R,generated.swift を生成
- R,generated.swift をプロジェクトに追加
こんな感じ
エラーになる場合は下記色々試すといいかも
- cmd + shift + K でクリーン
- ライブラリ/Developer/Xcode/DerivedData 内のファイルを削除(念の為ゴミ箱からも削除)
- Run script の位置を上の方に変える(Dependencies の下とか)
これが
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
let icon = UIImage(named: "settings-icon") let font = UIFont(name: "San Francisco", size: 42) let color = UIColor(named: "indicator highlight") let viewController = CustomViewController(nibName: "CustomView", bundle: nil) let string = String(format: NSLocalizedString("welcome.withName", comment: ""), locale: NSLocale.current, "Arthur Dent") // Storyboard let storyboard = UIStoryboard(name: "Main", bundle: nil) let initialTabBarController = storyboard.instantiateInitialViewController() as? UITabBarController let settingsController = storyboard.instantiateViewController(withIdentifier: "settingsController") as? SettingsController // Segue performSegue(withIdentifier: "openSettings", sender: self) override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let settingsController = segue.destination as? SettingsController, let segue = segue as? CustomSettingsSegue, segue.identifier == "openSettings" { segue.animationType = .LockAnimation settingsController.lockSettings = true } } // Cell override func viewDidLoad() { super.viewDidLoad() let textCellNib = UINib(nibName: "TextCell", bundle: nil) tableView.register(textCellNib, forCellReuseIdentifier: "TextCellIdentifier") } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let textCell = tableView.dequeueReusableCell(withIdentifier: "TextCellIdentifier", for: indexPath) as! TextCell textCell.mainLabel.text = "Hello World" return textCell } |
こうなる
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
let icon = R.image.settingsIcon() let font = R.font.sanFrancisco(size: 42) let color = R.color.indicatorHighlight() let viewController = CustomViewController(nib: R.nib.customView) let string = R.string.localizable.welcomeWithName("Arthur Dent") // Storyboard let storyboard = R.storyboard.main() let initialTabBarController = R.storyboard.main.initialViewController() let settingsController = R.storyboard.main.settingsController() // Segue performSegue(withIdentifier: R.segue.overviewController.openSettings, sender: self) override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if let typedInfo = R.segue.overviewController.openSettings(segue: segue) { typedInfo.segue.animationType = .LockAnimation typedInfo.destinationViewController.lockSettings = true } } // Cell override func viewDidLoad() { super.viewDidLoad() tableView.register(R.nib.textCell) } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let textCell = tableView.dequeueReusableCell(withIdentifier: R.reuseIdentifier.textCell, for: indexPath)! textCell.mainLabel.text = "Hello World" return textCell } |
swiftlint (0.39.2)
realm/SwiftLint リントツール!これも標準で入れていいと思う!!(自動修正もできるみたいだけど覚える意味もこめて私は使ってないです)
- Pods でインストール
- プロジェクトフォルダに .swiftlint.yml
- Run Script 追可
- Run Scriptに記載
if which ${PODS_ROOT}/SwiftLint/swiftlint >/dev/null; then
${PODS_ROOT}/SwiftLint/swiftlint
else
echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint"
fi
.swiftlint.yml はこんな感じ(〜Tests 系を除外するかは要相談)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
excluded: - Pods - Carthage - SampleSwiftAppTests - SampleSwiftAppUITests - ModelsTests - SampleSwiftApp/Resource/R.generated.swift opt_in_rules: - anyobject_protocol - array_init - closure_spacing - collection_alignment - conditional_returns_on_newline - contains_over_first_not_nil - discouraged_object_literal - empty_count - empty_string - identical_operands - joined_default_parameter - last_where - legacy_random - legacy_multiple - lower_acl_than_parent - modifier_order - operator_usage_whitespace - operator_whitespace - overridden_super_call - private_action - prohibited_super_call - redundant_nil_coalescing - trailing_closure - unavailable_function - unused_import - unused_private_declaration - vertical_parameter_alignment_on_call - vertical_whitespace_closing_braces - vertical_whitespace_opening_braces identifier-name: min_length: warning: 1 error: 1 function_parameter_count: warning: 6 line_length: warning: 300 error: 300 type_name: min_length: warning: 2 error: 2 max_length: warning: 50 error: 60 large_tuple: warning: 3 error: 4 disabled_rules: - force_cast - trailing_whitespace - force_try |
ルール詳細は公式をどうぞ
カスタムルール
カスタムのルールも作成でき形式は下記のような感じ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
custom_rules: pirates_beat_ninjas: # ルールID included: ".*\\.swift" # 適用範囲 excluded: ".*Test\\.swift" # 除外範囲 name: "Pirates Beat Ninjas" # ルール名(オプショナル) regex: "([n,N]inja)" # 正規表現 capture_group: 0 # ルール違反を強調する正規表現グループの数??(オプショナル)原文:number of regex capture group to highlight the rule violation at. match_kinds: # SyntaxKinds to match. (オプショナル). - comment - identifier message: "Pirates are better than ninjas." # メッセージ(オプショナル) severity: error # warningかerror(オプショナル) no_hiding_in_strings: # これはよくわからない?? regex: "([n,N]inja)" match_kinds: string |
match_kinds は下記。
- argument
- attribute.builtin
- attribute.id
- buildconfig.id
- buildconfig.keyword
- comment
- comment.mark
- comment.url
- doccomment
- doccomment.field
- identifier
- keyword
- number
- objectliteral
- parameter
- placeholder
- string
- string_interpolation_anchor
- typeidentifier
例えば下記のようにやると ~ViewModel.swift ファイルで import UIKit
をするとエラーをはく。(import と UIKit の間にスペース入れたりで逃げれるので厳密にやろうと思うともうちょっと工夫がいる)
1 2 3 4 5 |
custom_rules: yamete_import_uikit: included: ".*ViewModel\\.swift" regex: "(import UIKit)" message: "ViewModel で UIKit を import しないで下さい" |
一部ルールの有効無効
コード上でたまにここだけは無効にしたいとかあると思いますがそういうのもちゃんと用意されています。
最強のやつ
1 2 3 4 5 |
// これ以降のコードはルールを全部無視できる // swiftlint:disable all // これを書けばこれ以降からはルールを有効化できる // swiftlint:enable all |
指定のルールを無効化する
1 2 3 4 5 6 7 8 9 10 11 12 |
// 次の行の force_cast を無効化 // swiftlint:disable:next force_cast let noWarning = NSNumber() as! Int let hasWarning = NSNumber() as! Int // ここはルール有効 // この行の force_cast を無効化 let noWarning2 = NSNumber() as! Int // swiftlint:disable:this force_cast // 前の行の force_cast を無効化 let noWarning3 = NSNumber() as! Int // swiftlint:disable:previous force_cast |
注意下記のようにすると以降全て無効化される
1 2 3 4 5 |
// swiftlint:disable force_cast let noWarning1 = NSNumber() as! Int let noWarning2 = NSNumber() as! Int let noWarning3 = NSNumber() as! Int let noWarning4 = NSNumber() as! Int |
上のように this
とかを指定しない場合は enable
で挟んだ方がいい。(じゃないとせっかくのルールが。。。)
1 2 3 4 5 6 |
// swiftlint:disable force_cast let noWarning1 = NSNumber() as! Int let noWarning2 = NSNumber() as! Int // swiftlint:enable force_cast let hasWarning1 = NSNumber() as! Int let hasWarning2 = NSNumber() as! Int |
LicensePlist (2.15.1)
mono0926/LicensePlis ライセンス表示をめっちゃ簡単にしてくれるやつ!これも標準で入れていいと思う!!!
- Pods でインストール
- Settings.bundle を追加
- Root.plist 変更
- Run Script 追加
Settings.bundle を追加する。
Settings.bundle の Root.plist を下記のようにする。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>StringsTable</key> <string>Root</string> <key>PreferenceSpecifiers</key> <array> <dict> <key>Type</key> <string>PSGroupSpecifier</string> <key>FooterText</key> <string>Copyright</string> </dict> <dict> <key>Type</key> <string>PSChildPaneSpecifier</string> <key>Title</key> <string>Licenses</string> <key>File</key> <string>com.mono0926.LicensePlist</string> </dict> <dict> <key>Type</key> <string>PSTitleValueSpecifier</string> <key>DefaultValue</key> <string>1.0.0</string> <key>Title</key> <string>Version</string> <key>Key</key> <string>sbVersion</string> </dict> </array> <key>StringsTable</key> <string>Root</string> </dict> </plist> |
ついでにアプリのバージョンも表示するのに下記の Run Script を追加するといいと思う。
1 |
/usr/libexec/PlistBuddy -c "Set :PreferenceSpecifiers:2:DefaultValue ${MARKETING_VERSION}" "${BUILT_PRODUCTS_DIR}/${WRAPPER_NAME}/Settings.bundle/Root.plist" |
これで設定アプリに下記のように表示してくれる。
設定 | ライセンス一覧 | ライセンス |
---|---|---|
iOS 13 では設定アプリのバグがあり --single-page
オプションていうのも追加されました。(参考:iOS13の設定アプリにつまずいた)
DIKit (0.5.0)
ishkawa/DIKit DI 用にぜひ!!(Swinject/Swinject っていうのもあるみたいです)
- Carthage でインストール
- DIKit から zip をダウンロード
- zip 解凍
- ターミナルで解凍したディレクトリに移動
- ターミナルで
make install
コマンド実行 - Xcode で Run Script 追加
if which dikitgen >/dev/null; then
dikitgen ${SRCROOT}/$PRODUCT_NAME > ${SRCROOT}/$PRODUCT_NAME/Resource/AppResolver.generated.swift
else
echo "warning: dikitgen not installed, download from https://github.com/ishkawa/DIKit"
fi - ビルドして AppResolver.generated.swift を生成
- プロジェクトに AppResolver.generated.swift を追加
とりあえず適当に動かしてみる。
下記のように AppResolver.swift を追加する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import DIKit struct Hoge { let hogehoge: String } protocol AppResolver: Resolver { func provideHoge() -> Hoge } final class AppResolverImpl: AppResolver { func provideHoge() -> Hoge { return .init(hogehoge: "Hoge") } } |
適当に ViewController.swift とかに下記のように追加してみる
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import DIKit struct Piyo: Injectable { struct Dependency { let hoge: Hoge let foo: String } let hoge: Hoge let foo: String init(dependency: Dependency) { self.hoge = dependency.hoge self.foo = dependency.foo } } |
これでビルドすると AppResolver.generated.swift は下記のように生成される
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
import DIKit import UIKit extension AppResolver { func resolveHoge() -> Hoge { return provideHoge() } func resolvePiyo(foo: String) -> Piyo { let hoge = resolveHoge() return Piyo(dependency: .init(hoge: hoge, foo: foo)) } } |
あとは下記の DI プロトコルを使っていい感じに DI する!!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public protocol Injectable { associatedtype Dependency init(dependency: Dependency) } public protocol FactoryMethodInjectable { associatedtype Dependency static func makeInstance(dependency: Dependency) -> Self } public protocol PropertyInjectable: class { associatedtype Dependency var dependency: Dependency! { get set } } |
SwiftyBeaver (1.9.1)
SwiftyBeaver/SwiftyBeaver ログ出力におすすめ!!Mac アプリを使えば実機ログも確認できる!!!
- Carthage でインストール
- AppDelegate とかで初期化
- ログをはく
AppDelegate.swift のトップで下記のように初期化する。本番ではログ出力したくないので Debug のみにして出力先もコンソールのみに設定しています。
1 2 3 4 5 6 7 8 9 10 |
let log: SwiftyBeaver.Type? = { #if DEBUG let logger = SwiftyBeaver.self let console = ConsoleDestination() logger.addDestination(console) // コンソールにログを出力する return logger #else return nil #endif }() |
下記のようにするとクラウドとファイルにも出力できるらしい。クラウドの方は Mac アプリがありそこから見れるようです。(アカウント作成必要。フリープランもあり。)
1 2 3 4 5 |
let file = FileDestination() // Cachesディレクトリにswiftybeaver.logで保存される // file.logFileURL = URL(fileURLWithPath: "hogehoge") // これで出力先の変更ができる let cloud = SBPlatformDestination(appID: "foo", appSecret: "bar", encryptionKey: "123") // to cloud log.addDestination(file) log.addDestination(cloud) |
ログレベルは下記5つ
1 2 3 4 5 6 7 |
public enum Level: Int { case verbose = 0 case debug = 1 case info = 2 case warning = 3 case error = 4 } |
試しに出力してみるとこんな感じ
1 2 3 4 5 |
log?.verbose("Verboseログ") log?.debug("Debugログ") log?.info("Infoログ") log?.warning("Warningログ") log?.error("Errorログ") |
ログのフォーマット設定もできる(使ったことはないです)。試しに下記のようにしてみる。
1 2 |
let console = ConsoleDestination() console.format = "$DHH:mm:ss$d $L $M" |
こんな感じ
フォーマットは下記を設定できるらしい。
Variable | Description |
---|---|
$L | Level, for example "VERBOSE" |
$M | Message, for example the foo in log.debug("foo") |
$J | JSON-encoded logging object (can not be combined with other format variables!) |
$N | Name of file without suffix |
$n | Name of file with suffix |
$F | Function |
$l | Line (lower-case l) |
$D | Datetime, followed by standard Swift datetime syntax |
$d | Datetime format end |
$T | Thread |
$C | Color start, is just supported by certain destinations and is ignored if unsupported |
$c | Color end |
$U | Uptime in the format HH:MM:SS |
$X | Optional context value of any type (see below) |
Realm (5.0.3)
realm/realm-cocoa DB ならこれが楽!!
- Carthage でインストール
- Object 作成
- DB 処理
Realm Browser という Mac アプリがあり DB の中身も確認できる!と思ったけどなんかファイル開くときにキーを求められる。。。そして何も設定してないのでこのキーがわからない( Realm.Configuration.defaultConfiguration.encryptionKey
こいつか?と思ったけど nil
だった。。。)
調べると Realm Studio というのがあるらしいのでこっちを使おう!!
デフォルトだと Documents ディレクトリに default.realm が生成されるのでこれを取得する。
場所がわからなければ下記のようにログで確認できます。
1 |
print(Realm.Configuration.defaultConfiguration.fileURL!) |
初期化
保存先を変えたりするのは下記のように設定する
1 2 3 4 |
/// パターン1 var config = Realm.Configuration() config.fileURL = config.fileURL!.deletingLastPathComponent().appendingPathComponent("\(hoge).realm") Realm.Configuration.defaultConfiguration = config |
1 2 3 4 5 |
/// パターン2 let config = Realm.Configuration( fileURL: config.fileURL!.deletingLastPathComponent().appendingPathComponent("\(hoge).realm") ) let realm = try! Realm(configuration: config) |
読み込み専用ならプロジェクト内に relam ファイルを追加して下記のようにできる
1 2 3 4 5 |
/// 読み込み専用 let config = Realm.Configuration( fileURL: Bundle.main.url(forResource: "MyBundledData", withExtension: "realm"), readOnly: true) let realm = try! Realm(configuration: config) |
Configuration
のイニシャライザはこんな感じになっている(なんか前 deleteRealmIfMigrationNeeded
に true
が設定されててマイグレーションが変な感じになってたことがある。。。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
/** Creates a `Configuration` which can be used to create new `Realm` instances. - note: The `fileURL`, `inMemoryIdentifier`, and `syncConfiguration` parameters are mutually exclusive. Only set one of them, or none if you wish to use the default file URL. - parameter fileURL: The local URL to the Realm file. - parameter inMemoryIdentifier: A string used to identify a particular in-memory Realm. - parameter syncConfiguration: For Realms intended to sync with the Realm Object Server, a sync configuration. - parameter encryptionKey: An optional 64-byte key to use to encrypt the data. - parameter readOnly: Whether the Realm is read-only (must be true for read-only files). - parameter schemaVersion: The current schema version. - parameter migrationBlock: The block which migrates the Realm to the current version. - parameter deleteRealmIfMigrationNeeded: If `true`, recreate the Realm file with the provided schema if a migration is required. - parameter shouldCompactOnLaunch: A block called when opening a Realm for the first time during the life of a process to determine if it should be compacted before being returned to the user. It is passed the total file size (data + free space) and the total bytes used by data in the file. Return `true ` to indicate that an attempt to compact the file should be made. The compaction will be skipped if another process is accessing it. - parameter objectTypes: The subset of `Object` subclasses persisted in the Realm. */ public init(fileURL: URL? = URL(fileURLWithPath: RLMRealmPathForFile("default.realm"), isDirectory: false), inMemoryIdentifier: String? = nil, syncConfiguration: SyncConfiguration? = nil, encryptionKey: Data? = nil, readOnly: Bool = false, schemaVersion: UInt64 = 0, migrationBlock: MigrationBlock? = nil, deleteRealmIfMigrationNeeded: Bool = false, shouldCompactOnLaunch: ((Int, Int) -> Bool)? = nil, objectTypes: [Object.Type]? = nil) |
削除
DB ごと削除したい場合は下記のように .realm ファイル以外にも色々削除しないといけない。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
let realmURL = Realm.Configuration.defaultConfiguration.fileURL! let realmURLs = [ realmURL, realmURL.appendingPathExtension("lock"), realmURL.appendingPathExtension("note"), realmURL.appendingPathExtension("management") ] for URL in realmURLs { do { try FileManager.default.removeItem(at: URL) } catch { // handle error } } |
モデル
Realm で使えるデータ型は下記。
Type | Non-optional | Optional |
---|---|---|
Bool | @objc dynamic var value = false | let value = RealmOptional |
Int | @objc dynamic var value = 0 | let value = RealmOptional |
Float | @objc dynamic var value: Float = 0.0 | let value = RealmOptional |
Double | @objc dynamic var value: Double = 0.0 | let value = RealmOptional |
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: must be optional | @objc dynamic var value: Class? |
List | let value = List |
n/a: must be non-optional |
LinkingObjects | let value = LinkingObjects(fromType: Class.self, property: "property") | n/a: must be non-optional |
参考:realm - Property cheatsheet
こんな感じで定義する。
1 2 3 4 |
class Person: Object { @objc dynamic var name = "" @objc dynamic var age = 0 } |
let
は使えないみたい。(詳しくなんて書いてるかわからないけど dynamic var
のみみたいなんが書いてると思う。。。)
Realm model properties must have the @objc dynamic var attribute to become accessors for the underlying database data. Note that if the class is declared as @objcMembers (Swift 4 or later), the individual properties can just be declared as dynamic var.
主キーやインデックスも設定できる
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Person: Object { @objc dynamic var id = NSUUID().uuidString @objc dynamic var name = "" @objc dynamic var age = 0 override static func primaryKey() -> String? { return "id" } override static func indexedProperties() -> [String] { return ["id"] } } |
Ignoring properties っていうのもあるらしいけどどういうとき使うんだろ??DB には保存しないらしいです。
1 2 3 4 5 6 7 8 9 10 11 12 |
class Person: Object { @objc dynamic var tmpID = 0 var name: String { // これもignoreと一緒 return "\(firstName) \(lastName)" } @objc dynamic var firstName = "" @objc dynamic var lastName = "" override static func ignoredProperties() -> [String] { return ["tmpID"] } } |
多対1もしくは1対1の関係を表したい場合はプロパティに持たせるだけでOK
1 2 3 4 5 6 7 8 9 10 |
final class Dog: Object { @objc dynamic var id = NSUUID().uuidString @objc dynamic var name = "" @objc dynamic var age = 0 @objc dynamic var owner: Person? override static func primaryKey() -> String? { return "id" } } |
保存してみるとこんな感じ。Person
のデータを更新すると Dog
の owner
も反映されます。
多対多の場合は List
を使います。操作はほとんど Array
と同じ。
1 2 3 4 5 6 7 8 9 10 |
final class Person: Object { @objc dynamic var id = NSUUID().uuidString @objc dynamic var name = "" @objc dynamic var age = 0 let dogs = List<Dog>() override static func primaryKey() -> String? { return "id" } } |
双方向にデータをたどりたい場合 Person
に dogs
を定義していると Person
-> Dog
はたどれますが Dog
-> Person
はたどれません。仮に Dog
に owner
を定義しても owner
更新時に Person
の Dogs
を更新してはくれません。そういった場合は下記のように LinkingObjects
を利用します。(LinkingObject
を追加しても DB 上は Dog
テーブルに項目が増えたりはしません。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
final class Person: Object { @objc dynamic var id = NSUUID().uuidString @objc dynamic var name = "" @objc dynamic var age = 0 let dogs = List<Dog>() override static func primaryKey() -> String? { return "id" } } final class Dog: Object { @objc dynamic var id = NSUUID().uuidString @objc dynamic var name = "" @objc dynamic var age = 0 let owners = LinkingObjects(fromType: Person.self, property: "dogs") override static func primaryKey() -> String? { return "id" } } |
DB操作
1 2 3 4 5 6 7 8 9 |
final class Person: Object { @objc dynamic var id = NSUUID().uuidString @objc dynamic var name = "" @objc dynamic var age = 0 override static func primaryKey() -> String? { return "id" } } |
データ追加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
let realm = try! Realm() // 初期化色々 let person1 = Person() person1.name = "Hoge" person1.age = 10 // Dictinary利用 let person2 = Person(value: ["name" : "Piyo", "age": 3]) // 配列利用(idも設定しないといけない) let person3 = Person(value: [NSUUID().uuidString, "Foo", 15]) try! realm.write { realm.add(person1) realm.add(person2) realm.add(person3) // 配列で追加もできる // realm.add([person1, person2, person3]) } |
上記実行して Realm Studio で確認するとこんな感じ
データ更新
1 2 3 4 5 |
let realm = try! Realm() let person = realm.objects(Person.self).first try! realm.write { person.age = 20 // 更新 } |
こういう書き方もできる
1 2 3 4 5 6 |
let persons = realm.objects(Person.self) try! realm.write { persons.first?.setValue(true, forKey: "isFirst") // personsのplanetがすべてEarthに更新される persons.setValue("Earth", forKey: "planet") } |
下記のように update: .modified
を指定するとすでに同じ主キーのデータがある場合は更新、ない場合は追加になる。(主キーを設定していない場合は使えない。)
1 2 3 4 5 6 7 8 |
let dog = Dog() dog.name = "Inu" dog.age = 2 dog.id = "1" // 主キー try! realm.write { realm.add(dog, update: .modified) } |
下記のようにすると同じ主キーのデータがあれば age
のみ更新され、ない場合は name
がデフォルト値でデータが追加される。
1 2 3 |
try! realm.write { realm.create(Dog.self, value: ["id": "1", "age":12], update: .modified) } |
データ削除
1 2 3 4 5 6 7 8 9 |
try! realm.write { // 指定のデータを削除 realm.delete(dog) } try! realm.write { // すべて削除 realm.deleteAll() } |
データ検索
1 2 3 4 5 6 7 |
let allDogs = realm.objects(Dog.self) // NSPredicateも使えるけど基本的には使わなくてもいける let predicate = NSPredicate(format: "age > 3 AND name BEGINSWITH %@", "A") var dogs = realm.objects(Dog.self).filter(predicate) // 上と同じ結果 dogs = realm.objects(Dog.self).filter("age > 3 AND name BEGINSWITH 'A'") |
Predicate いろいろ
- 数値 と Date は ==, <=, <, >=, >, !=, BETWEEN を使える
objects.filter("name == %@", name)
とかできる- String と Data は ==, !=, BEGINSWITH, CONTAINS, ENDSWITH を使える
- LIKE も対応 ex.
value LIKE '?bc*
とか ? は1文字 * は 0以上の文字列を表す - IN も対応 ex.
name IN {'Hoge', 'Piyo', 'Foo'}
- AND, OR, NOT も使える
name == nil
で nil チェックもできるList
とResults
では @count, @min, @max, @sum, @avg も使える ex.objects.filter("dogs.@count > 5")
細かいのは realm - Filtering を確認。
検索結果は自動で更新される
1 2 3 4 5 6 7 8 9 10 |
let realm = try! Realm() let results = realm.objects(Dog.self).filter("name == 'Inu'") let dogs = Array(results) print("results: \(results.count)") // 1 print("dogs: \(dogs.count)") // 1 try! realm.write { realm.add(Dog(value: ["name" : "Inu", "age": 13])) } print("results: \(results.count)") // 2 <- 更新される!! print("dogs: \(dogs.count)") // 1 |
ソートもできる
1 2 |
var dogs = realm.objects(Dog.self).sorted(byKeyPath: "age") dogs = realm.objects(Dog.self).sorted(byKeyPath: "age", ascending: false) |
ドキュメントが充実してるのでその他詳細は realm ドキュメント をどうぞ。
Quick/Nimble (3.0.0/8.1.1)
Quick/Quick と Quick/Nimble ユニットテスト用におすすめ!!柔軟な表現力!!!エラーメッセージがとにかくわかりやすい!!!!
- Carthage でインストール(テスト用なので Cartfile.private でいい)
- テストを書く!
試しに Hoge
構造体のテストを書いてみます。
1 2 3 4 5 |
strunct Hoge { func hogehoge(isHoge: Bool) -> String? { return isHoge ? "hogehoge" : nil } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import Quick import Nimble @testable import PiyoTarget final class HogeSpec: QuickSpec { override func spec() { let hoge = Hoge() describe("hogehogeWithIsHoge method") { context("when isHoge is true") { it ("returns hogehoge") { expect(hoge.hogehoge(isHoge: true)).to(equal("hogehoge")) } } context("when isHoge is false") { it ("returns nil") { expect(hoge.hogehoge(isHoge: false)).to(beNil()) } } } } } |
こんな感じです。describe
がテスト対象で context
が条件で it
が期待する動作です。it
は describe
のことで it
の部分は hogehogeWithIsHoge method returns hogehoge when isHoge is true.
のような感じに文章になるはず。(英文として正しいか知らんけど。。。)
書き方は README に色々のってますが個人的によく使うのだけ書いときます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
let hoge = "HOGE" expect(hoge).to(equal("HOGE")) // 等値 expect(hoge).toNot(equal("PIYO")) // 否定 expect(hoge).notTo(equal("FOO")) // 否定 let isPiyo = true expect(isPiyo).to(beTrue()) // true expect(isPiyo).to(beFalse()) // false let foo: String? = "FOO" expect(foo).to(beNil()) // nil expect(foo).toNot(beNil())) // nilじゃない expect(foo).notTo(beNil()) // nilじゃない let piyoList = ["piyo", "hoge", "foo"] expext(piyoList).to(allPass { $0.count > 2 }) // 全部の要素が条件満たす expext(piyoList).to(haveCount(3)) // 要素数 expect(piyoList).to(contain("piyo")) // 含む expect(animal).to(beAnInstanceOf(Animal)) // クラス一致 expect(cat).to(beAKindOf(Animal)) // クラスorサブクラス一致 // エラー expect{ try (hogehoge()) }.to(throwError { (error: HogeError) in expect(error.localizedDescription).to(equal("it is hoge!")) }) |
一時的に無効化する場合は context
とかの前に x
をつける
1 2 3 |
xdescripe("hoge") {} xcontext("piyo") {} xit("foo") {} |
指定のやつだけ実行したい場合は context
とかの前に f
をつける
1 2 3 |
fdescripe("hoge") {} fcontext("piyo") {} fit("foo") {} |
すごい!日本語ドキュメントもあった!!
APIKit
ishkawa/APIKit 軽量な HTTP クライアント! Alamofire だと過剰かな?と思ったらこっち!!!通信周りは全部自前でやってもいいけど URL エンコードとかわりとめんどくさいのでライブラリに頼った方が無難です。
- Carthage でインストール
Request
プロトコルに準拠したリクエスト作成Session.send
で API をコール
Request
は下記
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
public protocol Request { /// レスポンスの型指定(必須) associatedtype Response /// ベースURL指定(必須) var baseURL: URL { get } /// HTTPのGETとか指定(必須) var method: HTTPMethod { get } /// URLのパス指定(必須) var path: String { get } /// レスポンスの処理(必須) func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Response /// パラメータ(オプショナル)、GETやPOSTとかみていい感じにしてくれる var parameters: Any? { get } /// クエリ(オプショナル)、parametersに任せてもいい var queryParameters: [String: Any]? { get } /// Body(オプショナル)、parametersに任せてもいい var bodyParameters: BodyParameters? { get } /// ヘッダー(オプショナル) var headerFields: [String: String] { get } /// パーサー(オプショナル)、何も設定しない場合は `response(from...)` でDictionaryが返る var dataParser: DataParser { get } /// Intercepts `URLRequest` which is created by `Request.buildURLRequest()`. If an error is /// thrown in this method, the result of `Session.send()` turns `.failure(.requestError(error))`. /// - Throws: `Error` func intercept(urlRequest: URLRequest) throws -> URLRequest /// Intercepts response `Any` and `HTTPURLResponse`. If an error is thrown in this method, /// the result of `Session.send()` turns `.failure(.responseError(error))`. /// The default implementation of this method is provided to throw `ResponseError.unacceptableStatusCode` /// if the HTTP status code is not in `200..<300`. /// - Throws: `Error` func intercept(object: Any, urlResponse: HTTPURLResponse) throws -> Any } |
実際に Codable
を利用して お天気Webサービス(Livedoor Weather Web Service / LWWS) で天気を取得してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
// デフォはDictionary返すので設定する struct CodableDataParser: DataParser { var contentType: String? { return "application/json" } func parse(data: Data) throws -> Any { return data } } enum WeatherRequest: Request { typealias Response = Weather case getWeather(city: String) var baseURL: URL { return URL(string: "http://weather.livedoor.com/forecast/webservice/json")! } var method: HTTPMethod { return .get } var path: String { return "v1" } var parameters: Any? { switch self { case .getWeather(let city): return ["city": city] } } var dataParser: DataParser { return CodableDataParser() } func response(from object: Any, urlResponse: HTTPURLResponse) throws -> Weather { guard let data = object as? Data else { throw ResponseError.unexpectedObject(object) } // ここもうちょっとどうにかしたかった return try JSONDecoder().decode(Weather.self, from: data) } } struct Weather: Decodable { public let title: String public let area: String public let city: String public let prefecture: String public let date: String public let dateLabel: String public let telop: String private enum CodingKeys: String, CodingKey { case title case forecasts case location } private enum ForecastsKeys: String, CodingKey { case date case dateLabel case telop } private enum LocationKeys: String, CodingKey { case area case city case prefecture } init(from decoder: Decoder) throws { let values = try decoder.container(keyedBy: CodingKeys.self) self.title = try values.decode(String.self, forKey: .title) // いらない情報あるのでこれでネストした値取得 let location = try values.nestedContainer(keyedBy: LocationKeys.self, forKey: .location) self.area = try location.decode(String.self, forKey: .area) self.city = try location.decode(String.self, forKey: .city) self.prefecture = try location.decode(String.self, forKey: .prefecture) // いらない情報あるのでこれでネストした値取得 var forecasts = try values.nestedUnkeyedContainer(forKey: .forecasts) let forecast = try forecasts.nestedContainer(keyedBy: ForecastsKeys.self) self.date = try forecast.decode(String.self, forKey: .date) self.dateLabel = try forecast.decode(String.self, forKey: .dateLabel) self.telop = try forecast.decode(String.self, forKey: .telop) } } // 天気取得 Session.send(WeatherRequest.getWeather(city: "020010"), callbackQueue: .main) { result in switch result { case .success(let response): print(response) case .failure(let error): print(error.localizedDescription) } } |
実装は簡単!処理結果が Result
なのもわかりやすくていい!!
もうちょっとどうにか抽象化したかったけど思いつかなかった。。。(関係ないけど Codable
でネストした値とれるの知らなかった!すげぇ!! nestedUnkeyedContainer
, nestedContainer
)
APIKit の処理を閉じ込めたくて APIKit.Request
に依存しない Request
を作ってアダプターみたいなので APIKit.Request
に変換してとか色々模索したけどここまでくると通信処理自作した方がいい気がしてきた。。。
Firebase/Analytics
アナリティクスでよくいれることになるやつ。設定めんどくさかったので今回は割愛します。。。
おわりに
このあたりのライブラリは UIKit に依存してるわけでもないので今後もしばらくは使っていくと思います。
使ったことないけどいいなと思うのが yonaskolb/XcodeGen。YAML でプロジェクトデータを記述して *.xcodeproj
ファイルを git 管理から外せるらしい。(コンフリクトが激減する!!)どっかで使ってみたい。
コメント
[…] ライブラリ編の続きです。 おすすめ構成を実践してみました。今回アーキテクチャに関しては特に触れないですがプレゼンテーションロジックをどっかに置きたいと思い PresentationModel というよくわからないものを置いています。 […]
[…] iOS みたいに Android のおすすめライブラリを紹介したいと思います。しかし、iOSと違って Android は実践したことはないです。。。(たぶんこれいいんじゃないかなぁ?って感じで書いてます) […]