- 投稿日:2019-09-27T22:35:43+09:00
SwiftでCの構造体をキャストしてUNIXドメインソケットを使う方法
SwiftでもCのシステムコールを呼ぶことができるのでうまく使えば、UNIXドメインソケットを使ったSwiftとCのライブラリ間のスレッド通信を行うことができる。
Cでの操作方法
C言語でUNIXドメインソケットを使う場合にはsockaddr_un構造体をsockaddrにキャストしてbindやconnectに渡す必要がある。
int sock = socket(AF_UNIX, SOCK_STREAM, 0); struct sockaddr_un addr; strcpy(addr.sun_path, "/tmp/hogehoge"); addr.sun_family = AF_UNIX; bind(sock, (struct sockaddr *)&addr, sizeof(addr));Swiftでの操作方法
Swiftで同様にキャストを行なう必要があるが、
let sockAddrUn = sockAddr as sockaddr_un
のようなキャストはできない。
Swiftの場合は、sockaddr_un構造体 → UnsafeRawPointer化 → assumingMemoryBoundを使ってsockaddr構造体に変換
のようなステップを踏む必要がある。
具体的には、次のように書く
let sock = socket(AF_UNIX, SOCK_STREAM, 0) let sockPath = "/tmp/hogehoge" var sockAddrUn = sockaddr_un() sockAddrUn.sun_family = sa_family_t(AF_UNIX) sockPath.withCString { path in Darwin.memcpy(&(sockAddrUn.sun_path), path, Int(strlen(path))) } let rawPtr = UnsafeRawPointer(&sockAddrUn) let sockAddrPtr = rawPtr.assumingMemoryBound(to: sockaddr.self) Darwin.bind(sock, sockAddrPtr, socklen_t(MemoryLayout<sockaddr_un>.stride))こんな書き方は絶対ダメ
因みに最初に構造体のキャスト方法がわからず変数を二つ用意してmemcpyをするという荒技を行なっていた。
let sock = socket(AF_UNIX, SOCK_STREAM, 0) let sockPath = "/tmp/hogehoge" var sockAddrUn = sockaddr_un() sockAddrUn.sun_family = sa_family_t(AF_UNIX) sockPath.withCString { path in Darwin.memcpy(&(sockAddrUn.sun_path), path, Int(strlen(path))) } var sockAddr = sockaddr() Darwin.memcpy(&sockAddr, &sockAddrUn, MemoryLayout<sockaddr_un>.stride) Darwin.bind(sock, &sockAddr, socklen_t(MemoryLayout<sockaddr_un>.stride))sockaddr_unの分だけコピーしてbindにsockaddr_unの長さを渡すのでビルドは通るし、動きはするがReleaseビルドで最適化設定がされるとsockaddrからはみ出た分のデータが消えてしまい、bindがEAFNOSUPPORTエラーを返すようになる。
A構造体 → UnsafeRawPointer → assumingMemoryBoundでB構造体 でキャストするようにしましょう。
- 投稿日:2019-09-27T18:50:26+09:00
iOSのLocalDB系をまとめてみた
概要
オブジェクトの永続化するためのローカルDB系についてメリデメなどを諸々調べてみた!
https://jmty.kibe.la/@misato_naruse/5483 こっちも書いたので合わせて比較対象
- UserDefaults
- SQLite
- CoreData
- SQLiteマッパーライブラリ
- Realm
▼CoreDataとSQLiteのマッパーライブラリとの違い
CoreDataはSQLiteのマッパーとはちょっと違う前提
- ジモティーではUserDefaultsを使用
- ↑使用用途:ユーザデータや検索情報の保存
▼UserDefaults
概要
特徴
- key=valueでデータを保存する
- KVO形式のため、オブジェクトを直接呼び出さずにデータの保存と取得ができる
- plistにXML形式で保存される
- スレッドセーフ
- アプリ起動時にメモリに読み込まれ、以降キャッシュを利用するため処理が高速
他との違い
- データの絞り込みができない
- SQLiteのWHEREやRealmのfilterなどのように使用できない
- 大規模なものには向いてない
- key=valueのためカラム等の概念とは異なるため、 ユーザ1、ユーザ2..ユーザ100と増えていくデータの保存には上に理由も合わせて向いていない
- 保存できる型が決まっている
- NSData、NSString、NSNumber、NSDate、NSArray、NSDictionary
- それ以外を保存したい場合はNSDataに変換する
基本の書き方
// 保存 UserDefaults.standard.set("保存したい値", forKey: "data") // 取得 let date = UserDefaults.standard.integer(forKey: "data")▼SQLite
CoreData
概要
- 2010年:iOS 3.0+
- データの構造が複雑で、アプリからユーザーの入力した情報を保存したり、任意の条件で入力した情報の検索結果を返したい場合などにUserDefaultsではなくこちらを使用する
特徴
swiftライクでコードが書けながらもSQLiteの機能を使うことができる
swift
let fetchRequest = NSFetchRequest(entityName: "RegistData")
let registData = NSPredicate(format: "id = %d", 1)
fetchRequest.registData = registData
do {
let users = try appDelegate.managedObjectContext.executeFetchRequest(fetchRequest) as! [User]
print(users) // データの絞り込み
}
↑でエンティティを追加すると自動的に以下のようなモデルクラスが作成される
import CoreData class RegistData: NSManagedObject { @NSManaged var id: NSNumber? @NSManaged var registData: String? }
- メモリ節約機能(フォールト)が備わっている
- プロパティにアクセスした際に初めてデータを読み込む
- 節約にはなるが、逆にデータがうまく更新されないというデメリットもある
- 使いやすくするMagicalRecordという外部ライブラリがある
マイグレーションやり方
軽微な変更
→LightWeightMigration
```swift
lazy var persistentContainer: NSPersistentContainer = {
let container = NSPersistentContainer(name: "coreDataTest")
let description = NSPersistentStoreDescription()
description.shouldMigrateStoreAutomatically = true
description.shouldInferMappingModelAutomatically = true
container.persistentStoreDescriptions = [description]container.loadPersistentStores(completionHandler: { _, error in if let error = error as NSError? { fatalError("Unresolved error \(error), \(error.userInfo)") } }) return container}()
```既存スキーマを変更したい場合:新規プロパティ追加だけであれば↑を書けば以降の処理は自動で行ってくれる
複雑な変更
- →ManualMigration
- 上のコードを記述+マッピングマイグレーション(新しくスキーマを作成する)を行う
- 新しく追加したEntityにSubclassとして既存Modelを紐づける
他との違い
⇆UserDefaults
- スレッドセーフでない
- CoreDataのデータオブジェクトを管理するクラス(ManageObjectContext)
- そのため各スレッドでManageObjectContextを作成する必要がある #### ⇆SQLite
- 自身が自身を管理して永続性を保証するオブジェクトを作り出す
- 上で書いたモデルクラスのこと。EntityとModelが紐づいていることが保証されている #### ⇆Realm
- どっちもマイグレーションは基本自動でやってくれるが、CoreDataはModelと紐づいているので既存データの
基本の書き方
/// AppDelegate.swift func applicationWillTerminate(_ application: UIApplication) { self.save() } func save() { let context = persistentContainer.viewContext if context.hasChanges { do { try context.save() } // 略 } } /// VC let context = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext var registData: RegistData? var registDataArray: [RegistData] = [] // 保存 @IBAction func tapRegisterButton(_ sender: UIButton) { self.registerData = RegisterData(context: self.context) self.registerData?.registerData = "保存したい値" self.registerDataArray.append(self.registerData!) (UIApplication.shared.delegate as! AppDelegate).save() } // 取得 do { self.registDataArray = try self.context.fetch(RegistData.fetchRequest()) } // 略SQLiteマッパーライブラリ
概要
- iOS標準搭載されているDBMSライブラリ
- 基本的にFMDB,SwiftData,GRDB.swiftなどのラッパーライブラリを合わせて使用する
- ↑これらのライブラリを使わずに直接
sqlite3.h
をインポートして記述することも可能■FMDB
- podsではなくローカルインポート
- +
Bridging-Header.h
経由で使用する必要がある- 結構SQLite寄りのコード
- マイグレーションに関しての記事が全然見当たらなかったけど、別途専用のライブラリを使うとかは必要...? ```swift /// Bridging-Header.h #import "FMDatabase.h" を追加
/// Repository
var paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
var dirPath = paths.first! as String
var path = dirPath.stringByAppendingPathComponent("xxx.db")
let db = FMDatabase(path: path)// 取得
var resultSet = db.executeQuery("SELECT * FROM notes", withParameterDictionary: nil)
while resultSet.next() {
var data = resultSet.stringForColumn("data")
}// 保存
var now = NSDate()
var updateSQL =
"UPDATE notes SET name = :name,price = :price,updated_at = :updated_at"var params = ["data": "保存したい値"]
self.db.executeUpdate(updateSQL, withParameterDictionary: params)
```■SwiftData
- FMDBの次に主流ぽかったけど、swift3以降に対応してないみたい
■GRDB.swift
- 2018年にでた新しめのライブラリ
- cocoaPods
- マイグレーション機能を搭載
- KVO形式で保存取得できる
- RxGRDBってのもあるみたい ```swift /// podsfile use_frameworks! pod 'GRDB.swift'
/// Repository
let inMemoryQueue = DatabaseQueue()
let queue = DatabaseQueue(path: "path")
let pool = DatabasePool(path: "path")
let queue = DatabaseQueue()
do {
try queue.inDatabase{ (db) in
try db.create(table: User.databaseTableName) { (t) in
t.column("id", .integer).primaryKey(onConflict: .ignore, autoincrement: true)
t.column("name", .text).notNull()
t.column("age", .integer).notNull()
t.column("score", .integer).notNull()
t.column("created_at", .date).notNull()
}
// 取得
try User.fetchOne(db, key: data)
}
}// Rxで記述
User.all().rx
.fetchAll(in: queue)
.subscribe(onNext: { (users) in
// ここで処理
})// ageが20のユーザーのみに絞る
User.filter(User.Columns.age == 20)
.rx.fetchAll(in: queue)
.subscribe(onNext: { (users) in
// ここで処理
})/// SQLiteっぽく書くのもOKらしい
do{
try GlobalData.dbQueue.inDatabase { db in
let rows = try Row.fetchCursor(db, "SELECT * FROM hogehoge_table")
while let row = try rows.next() {
if row.value(named: "hogehoge_field1") != nil {
let hogehoge_int = row.value(named: "hogehoge_field1") as Int
let hogehoge_string = row.value(named: "hogehoge_field2") as String
}
}
}
}
```▼Realm
概要
- 2015年〜
- SQLiteを使わない独自のDBライブラリ(NOSBD(NotOnlySQL))
特徴
- 処理速度が速い
- ドキュメントが手厚い
- Android版もあるクロスプラットフォーム
マイグレーションやり方
let config = Realm.Configuration(schemaVersion: 1) // 新しいスキーマバージョンを指定する let realm = try! Realm(configuration: config) // プロパティの新規追加だけであれば以下の処理は不要 Realm.Configuration.defaultConfiguration = Realm.Configuration( schemaVersion: 1, migrationBlock: { migration, oldSchemaVersion in // 初めてマイグレーションを実行するのでoldSchemaVersionは0です if (oldSchemaVersion < 1) { // 名前の変更は`enumerateObjects(ofType: _:)`の外側で実行する必要があります。 migration.renameProperty(onType: Person.className(), from: "yearsSinceBirth", to: "age") } })
- Modelの中身が新規プロパティ追加だけであれば以降の処理は自動で行ってくれる
- 旧データを自動で移行する
- もしAとBを足したCを新規プロパティ追加したい場合など既存データを変更したい場合はmigrationBlockを実行する
基本の書き方
- Pods/Carthegeで導入 ```swift // 保存 /// AppDelegate.swift func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // Reaml暗号化 let key = "xxxxxxxxxxx" let configuration = Realm.Configuration(encryptionKey: key.data(using: .utf8, allowLossyConversion: false)) Realm.Configuration.defaultConfiguration = configuration }
/// Model
import RealmSwift
class RegisterData: Object {
dynamic var id = UUID().uuidString
dynamic var registData: String?
override class func primaryKey() -> String? {
return "id"
}
}/// Repository
import RealmSwift
// 保存
func save() {
let registData: RegistData?
do {
let realm = try Realm(configuration: Realm.Configuration.defaultConfiguration)
try realm.write {
registData.date = "保存したい値"
}
}
}
// 取得
func get() {
let registData: RegistData?
do {
let realm = try Realm(configuration: Realm.Configuration.defaultConfiguration).object(self).first
try realm.write {
registData.date = realm.data
}
}
}
```比較
SQLiteとRealm
参考
CoreDataとRealm
- どっちもスレッドセーフじゃないため各スレッドでオブジェクトを生成する必要がある
なるせの考察
- UserDefaults:今くらいの目的であれば必要十分かも
- CoreData:swiftyにSQLite使えるのはそさそ。スキーマの管理も可視化できるのは個人的に好き
- ただ、あんま理解してないけどスレッド周りが怖そうだし、導入自体のハードル高そう
- FMDB(SQLite):古い文献しかなくて辛かったし、マイグレーションあたりが未知数
- GRDB.swift(SQLite):良さそう。CoreDataよりも簡単そう
- Realm:大量のデータを運用するわけじゃないから、あんま意味なさそうだ。書くのは楽そう
→結論:UserDefaultsでいいのかも..(´・ω・`)
最後に
難しいよう.....(´;ω;`)
- 投稿日:2019-09-27T18:47:46+09:00
よちよちSwiftエンジニアのコードレビュー備忘録
これはSwift歴4ヶ月の自分が、ベテランiOSエンジニアの方にがっつりコードレビューしていただいた際の備忘録です。今後、良くないコード書かないために、また自分がレビューする際に適切に指摘できるようになるために、内容をまとめることにしました。
コーディングルールは、組織によって違いがあると思います。これから紹介するレビューでのルールが絶対というわけではありません。この内容を参考にしていただき、より良いコーディングルール、レビューの行ってくれる方々が少しでもいてくれれば幸いです。
アクセス修飾子を適切に設定する
Swiftではアクセス修飾子(public、internal、private、fileprivate、open)を省略した場合、internalを同じ意味になります。internalは「同一モジュール内であればアクセス可能」です。internalだとアクセス範囲が広すぎる場合があるので、必要に応じて適切に設定します。
以下、例です。
UIViewControllerのUIのOutletにprivateをつける
class StartViewController: UIViewController { /// タイトルのラベル @IBOutlet private weak var titleLabel: UILabel! /// スタートボタン @IBOutlet private weak var startButton: UIButton! }titleLabel、startButtonそれぞれに「private」が設定されています。privateがない場合、ViewControllerの外からtitleLabel、startButtonの値が書き換えることが可能になっています。そういった要件が必要でない場合は「private」などの修飾子を設定し、値を書き換えられないようにします。
継承してほしくないclassにはfinalをつける
final修飾子をclassに設定すると、設定されたclassは継承が不可になります。継承を前提としないclassにfinalを付けないと、意図しないところで継承が行われることで、結果コードの可読性、保守の難易度が不必要に上がります。そういった事態を防ぐため、継承しないclassにはfinal修飾子を設定します。
// NextViewControllerは継承不可 final class NextViewController: UIViewController { }不要なコメントアウトされたコードは削除する
実装を進めていく中で、不必要になったコードをコメントアウトすることがあるかと思います。しかしコメントアウトされたコードが残っていると、時間が経つうちに、そのコードがアプリケーションにとって本当に必要なのか、コードを見ても判断できなくなっていきます。結果コードの可読性が落ち、保守が難しいプログラムになってしまいますので、不要なコメントアウトされたコードは積極的に削除します。
変数宣言の際、letとvarでは基本letを使う
letで宣言された変数は、一度値を設定すると後から書き換えることができない変数です。
let a = 1 a = 2 // error反対にvarで宣言された変数は、後でも値を書き換えることができます。
var b = 2 b = 10 // ok!値の再代入の必要がないにもかかわらずvarを使ってしまうと、コードを読む際「この変数は値が変わる可能性があるから、値の変化を追いかける必要がある」と認識させてしまう可能性があり、不必要にコードの可読性が下がります。変数宣言の際には、基本的にはletを使うようにします。
デコード処理で「!」は使わない
jsonのデコード処理の際、値のキャストが必要になったことがあるのですが、キャストに失敗したときにアプリが落ちるような実装を行ってました。
extension Coordinate { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) // とりあえずDoubleで変換できない値がAPIから来た場合はクラッシュしてもらう x = Double(try container.decode(String.self, forKey: .x))! y = Double(try container.decode(String.self, forKey: .y))! } }しかしデコード処理のエラー処理の適切な書き方をレビューで教えて頂きましたので、今後はこの書き方で実装を行います。
extension Coordinate { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) // キャストに失敗した場合はDecodingError.typeMismatchの例外を投げる // エラーのハンドリングが丁寧でわかりやすい let rawX = try container.decode(String.self, forKey: .x) guard let _x = Double(rawX) else { throw DecodingError.typeMismatch( String.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Can not convert to Double type.")) } x = _x let rawY = try container.decode(String.self, forKey: .y) guard let _y = Double(rawY) else { throw DecodingError.typeMismatch( String.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Can not convert to Double type.")) } y = _y } }Binderを積極的に利用する
Binderを利用しない場合ViewModelからデータを取得しUIに渡すコードは、一例ですが以下のような書き方になります。複雑になればなるほど、viewDidLoadが肥大化してしまい、可読性が落ちます。
override func viewDidLoad() { super.viewDidLoad() let output = viewModel.transform() // 処理が長い... output.response.drive(onNext: { [unowned self] res in self.dateLabel.text = res.date self.dayOfTheWeekLabel.text = res.dayOfTheWeek self.hourLabel.text = res.hour self.salaryLabel.text = res.salary }).disposed(by: disposeBag) }Binderを利用することで、viewDidLoadのコードの見通しが良くなり、可読性が上がります。
override func viewDidLoad() { super.viewDidLoad() let output = viewModel.transform() // bindingの記述がシンプルに output.response.drive(binding) .disposed(by: disposeBag) } // binderの処理を別関数で実装 private var binding: Binder<Response> { return Binder(self, binding: { (vc, res) in vc.dateLabel.text = res.date vc.dayOfTheWeekLabel.text = res.dayOfTheWeek vc.hourLabel.text = res.hour vc.salaryLabel.text = res.salary }) }さいごに
エンジニア4年目にして初めてコードレビューしていただける機会を頂けたのですが、本当に勉強になりましたし、レビューして頂いたエンジニアの方には感謝しかありません。この場をお借りして御礼申し上げます。
参考
- 投稿日:2019-09-27T18:45:17+09:00
URLSession系の優良記事まとめ
JSON
・非エンジニアに贈る「具体例でさらっと学ぶJSON」 | DevelopersIO
→オブジェクト
,配列
,キー
,値
らへんがごっちゃな方はこの記事から
・[Swift3] JSON文字列の作成 | DevelopersIO
・Swift3でJSONパースを行う - QiitaURLSession
・Swiftで遊ぼう! - 482 - URLSessionの基礎 - Swiftで遊ぼう! on Hatena
→サンプルなどを用いた説明
・【Swift】URLSessionまとめ - Qiita
→関数などのまとめ
- 投稿日:2019-09-27T18:10:00+09:00
Xcode11で古いプロジェクトテンプレートを利用する
動機
先日iOS13がリリースされましたが、アプリを新規に始める場合に、iOS11〜1OS13あたりをターゲットに設定することは、まだまだあるかと思います。
開発では、新しいXcode11をメインに使用したいのですが、
Single View App
テンプレートでプロジェクトを新規作成した場合、そのままターゲット設定をiOS13未満にするとScenes関連のclassが使用できないためコンパイルエラーがでます。
Scenesとは、iOS13で登場したひとつのアプリプロセスに対してマルチUI(マルチウインドウ)をサポートするための仕組みのようで、現状では主にiPadでの用途があるようです。これによってSceneDelegate.swift
と関連設定が、デフォルトのプロジェクトに含まれているためです。ターゲットiOSバージョンによってはXcode11で、Scenes関連の設定が含まれないXcode10バージョンの
Single App Viewテンプレート
もまだまだ使いたい時があるなあ、と思ったのでXcode10からコピーしてXcode11からも使えるようにしておけば便利かもと思った次第です。というわけで、当記事ではXcodeのカスタムテンプレート追加手順を、表題の件に絞ってまとめておきます。
そもそもそういう頻度が少ない場合
そんなにプロジェクト新規作成の頻度が少ない場合、後述するテンプレートのコピーをしなくても、Xcode10&11併用環境であればiOS12〜以下をターゲットする場合、普通にXcode10.3でプロジェクトを新規作成して開始すれば大丈夫です。そうでない場合は、新規作成のためだけに古いXcodeを保持しておかなければならない、というデメリットがあります。(マイグレーションが必要な時などに役には立つのですが)
または手作業でXcode11で作成したプロジェクトの
SceneDelegate.swift
などを消すことも可能。しかし何度も行う場合には省略したいとも思います。
- Xcode 11 - Opt out of UISceneDelegate/SwiftUI on iOS 13
- How to remove SceneDelegate code from iOS 13 project?テンプレートのコピー手順
では、Xcode10から古いタイプの
Single View App
のプロジェクトテンプレートをコピーしてきて独自のテンプレートとして、Xcode11で使えるようにします。作業後の完成イメージのキャプチャですが、作業後にはプロジェクト作成ウィザードに画像のような形で選択出来るようになります。
カスタムテンプレート用のフォルダを作成
Xcode独自テンプレートは以下のパス配下に追加する決まりになってます。
# 独自Projectテンプレートのパス ~/Library/Developer/Xcode/Templates/Project\ Template # 独自Fileテンプレートのパス ~/Library/Developer/Xcode/Templates/File\ Templateテンプレートを置くフォルダを作成。テンプレートのカテゴリ表示はフォルダ名が反映されるので
My Templates
という名前のフォルダを作成することにします。$ mkdir -p ~/Library/Developer/Xcode/Templates/Project\ Templates/My\ TemplatesXcode10.3からのテンプレート複製
フォルダの準備ができましたので、次は中身です。プロジェクト新規作成ウィザードに表示されるテンプレートは
Xcode.app
パッケージを開いた中に入ってますので、そいつをコピーします。Xcode10.3がない場合はダウンロードしてくる必要があります。Xcode.appを右クリックからパッケージの内容を表示
で辿っていくか、以下のコマンドを実行して開きます。# パスの中の`Xcode_10.3.app`部分は適宜変更してください。 open /Applications/Xcode_10.3.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/Project\ Templates/iOS/Application/Finderで操作するか、以下のコマンドを実行し
Xcode_10.3.app
のパッケージの中からSingle View App.xctemplate
をカスタムテンプレート用フォルダへとコピーします。cp -r /Applications/Xcode_10.3.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/Xcode/Templates/Project\ Templates/iOS/Application/Single\ View\ App.xctemplate ~/Library/Developer/Xcode/Templates/Project\ Templates/My\ Templates/Single\ View\ App.xctemplate追記(2019/09/29):
[重要] テンプレートのIdentifierがXcode11のオリジナルと重複していると、ウィザードにオリジナルの方のSingle View Appアイコンが表示されなくなってしまっていたので、
コピーした方のSingle View App.xctemplate
の中のTemplateInfo.plist
ファイルを開いて(Xcodeが起動するはず)Identifier
部分を何か好きな文字列に編集を行います。(↓画像の例ではオリジナルの文字列に_Xcode10.3
をを付加しました。)作業は以上で完了です。
確認
Xcode11を開いて、プロジェクト新規作成のウィザードを起動します。
File -> New -> Project...
を選択か、または、ショートカットキーでCmd + Shift + N
です。Xcode10からカスタムテンプレートとしてコピーした
Single View App
がMy Templates
項目に表示されており、選択するとScenes関連なしのプロジェクトを開始出来るようになっているはずです。まとめ
Xcodeでは、最新のAPIに合わせたテンプレートへ変わることは珍しくないように思いますので、過渡期期間では今後も役に立つかもしれません。
参考?
- 投稿日:2019-09-27T17:36:11+09:00
BrightFuturesのレスポンス結果を表示するのはonSuccessのスコープ内でやろう
BrightFuturesを使用した非同期処理の場合、
APIから取得した結果を表示するためにUILable等に設定するのは、onSuccess
のスコープ内でやること。
そうしないと非同期処理のため、値がまだ帰ってきていない内に描画処理が走り、想定通りに反映されない。普段JavaScriptで書いてるときは自然としてそうだが失念してた。
Async/Awaitほしい、、class SomeViewController: AutoLayoutViewController { private var someObj:SomeObject = SomeObject() private var someLabel: UILabel override func viewDidLoad() { [BrightFuturesを使った非同期処理] .onSuccess { success in someObj = success // ここが正解 self.someLabel.text = success.text } // ここでやると反映されない // self.someLabel.text = success.text } }
- 投稿日:2019-09-27T17:08:08+09:00
react-native Swift共同開発メンバー募集!!
共同開発メンバー募集
性的嗜好によってマッチングするマッチングアプリ『sheep』開発中!!
1:現在創業間もない会社でアプリを開発しています。
2:我々はこの領域で世界一を目指しています!
3:マッチングアプリを一緒に開発してくれるエンジニアの方を募集しています
4:現在、資金調達中です。(https://yay2019.work/)アプリ開発したい方、
ゼロイチで開発したい方
グローバルカンパニーを一緒に作りたい方
まずはランチなど一緒に行きませんか?
一緒にグローバルカンパニーを作りましょう!!
サービスの概要は下記からご覧ください。https://drive.google.com/file/d/1_lz-XKQBn6tWrav2fKGLztuZa1-qptm5/view?usp=sharing![sheep.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/309610/39640d66-a52a-a6fb-d2a7-8e566ed5e9ca.jpeg)
- 投稿日:2019-09-27T16:45:38+09:00
Reactorのtransform内でPublishSubjectをマップした挙動をテストした際にハマったこと
transform(mutation:)
内の処理以下のように
event
をマップしたReactor内のtransformの挙動をテストする際につまづいたポイントを書き残しておきます。import ReactorKit import RxSwift final class HogeReactor: Reactor { ... func transform(mutation: Observable<Mutation>) -> Observable<Mutation> { return .merge( mutation, provider.accountService.event.flatMap { event -> Observable<Mutation> in switch event { case let .updateProfile(user): return .just(.setUser(user)) } } ) } ... }
event
の実装ちなみに
provider.accountService.event
は以下のようにPublishSubject
で実装されています。enum AccountEvent { case updateProfile(user: User) } protocol AccountServiceType { var event: PublishSubject<AccountEvent> { get } ... } final class AccountService: AccountServiceType { let event = PublishSubject<AccountEvent>() ... }テスト失敗
これをテストする際に単純にstreamを流しただけだとテストがうまくいきませんでした。
@testable import Hoge import Nimble import Quick class HogeReactorSpec: QuickSpec { ... describe("state.user") { context("when receives updateProfile event", closure: { it("is updated user", closure: { let updatedUser = TestData.testUser() stubServiceProvider.accountService.event.onNext(.updateProfile(user: updatedUser)) // Failed:stateにupdatedUserがセットされず expect(hogeReactor.currentState.user) == updatedUser }) }) } }一度も
transform(mutation:)
が呼ばれてないため、event
のマップがそもそもできていないのが原因ぽいです。テスト通過
transform(mutation:)
を呼ぶために適当にactionを呼んでやるとうまくいきました。@testable import Hoge import Nimble import Quick class HogeReactorSpec: QuickSpec { ... describe("state.user") { context("when receives updateProfile event", closure: { it("is updated user", closure: { // NOTE: Call any action to set up transformed mutation hogeReactor.action.onNext(.load) let updatedUser = TestData.testUser() stubServiceProvider.accountService.event.onNext(.updateProfile(user: updatedUser)) // Pass:stateにupdatedUserがセットされる expect(hogeReactor.currentState.user) == updatedUser }) }) } }本当にこれが正しいのかわかりませんが、とりあえず動いたので良しとしてます。
知見ある方いましたらコメントで教えていただけると嬉しいです。
- 投稿日:2019-09-27T12:14:25+09:00
Xcode11で作成したプロジェクトからSceneに関する機能をオミットする
Xcode11とUISceneとUIWindowにまつわるトラブルシュートに追われている。
個別のケースに分解してお届け。背景
Xcode10で作成したプロジェクトをXcode11で開いた場合と、Xcode11で作成したプロジェクトで
UIWindow
の振る舞いが異なった。この違いはプロジェクトがScene Based Lifecycleに対応しているかどうかに左右されることが分かった。対応
簡単に言うと以下を実施すればよい。
AppDelegate
にwindow
を生やす- Info.plistから
Scene
に関する項目を取り除くAppDelegate
からScene
に関する項目を取り除くこれにより、iOS13で動かしてもAppDelegateを基本としたアプリのように動作する。(View Hierarchyを見るとUISceneは健在なので、互換性を保っているだけっぽい)
AppDelegate
にwindow
を生やすSceneに関連する物事をオミットしたあと、AppDelegateにwindowプロパティが無いとブラックアウトしてしまうので、生やす。
// AppDelegate.swift @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? }// AppDelegate.h @interface AppDelegate: UIResponder <UIApplicationDelegate> @property (nonatomic, strong) UIWindow *window; @endInfo.plistから
Scene
に関する項目を取り除くInfo.plist から [Application Scene Manifest] の項目を取り除く。
ここまでの対応だけだと、iOS13でアプリを実行した場合に表示すべきStoryboardの情報が見つけられずブラックアウトする。
AppDelegate
からScene
に関する項目を取り除くAppDelegateから、Scene Based Lifecycleに関するメソッドを取り除くことで、オミットが達成される。
具体的には
AppDelegate application:configurationForConnectionSceneSession:
を取り除けば良い。
- 投稿日:2019-09-27T11:41:58+09:00
[Xcode11]カスタムトランジションが遷移しなくなった時の対処法
久々の投稿です。パッと見ではヒットしなかった話題なのですが、ハマる人もいる気がするので軽めに書きます。
何が起こったか
UIViewControllerTransitioningDelegate
を利用してトランジションアニメーションを導入していたアプリをXcode11でビルドしたところアプリが落ちないのに遷移が正常に動作せず、アプリが操作不可になる現象が発生した前提知識
iOS13からモーダルのデフォルトの挙動がハーフモーダルのような形に変化したのはよく知られていることだと思います。
これにあわせてsegueの遷移には
このように今までのTransitionの設定に加えPresentationという項目が増えているのがわかります。ここを設定することで今までのようなフルスクリーンのモーダルに変更したりすることができます。解決法
この増えた設定項目が肝でした。カスタムトランジションの場合、このPresentationとともに増えたコンテキストの概念が迷子になるらしくそれが影響して遷移がうまく完了しないということのようでした。
ですのでシンプルにこのようなコードを入れてあげると解決します。
let viewController = NextViewController.instatiate() viewController.modalPresentationStyle = .currentContext present(viewController, animated: true, completion: nil)このように明示的に
modalPresentationStyle
を設定すれば解決しました。
ちなみに僕の場合はcurrentContext
もしくはfullScreen
で動くようになりましたが、これはトランジションの種類にもよるかと思います。ぜひ同じ症状にあった人は様々な項目を試してみてください。(本当は意味的にどれを選ぶべきとかありそうですが一旦...笑)
- 投稿日:2019-09-27T10:42:08+09:00
ios開発、addTargetメソッドを調べてみた
addTargetの引数
target アクションメソッドが呼ばれるオブジェクトを指定。 action 呼ばれるメソッドを記述 controlEvents UIControlEventsの定数を記述 controlEventsに入るUIControlEventsには以下がある
touchDown コントロール内でのタッチダウン touchDownRepeat 複数回のタッチダウンイベント touchDragInside コントロール内で指がドラッグされた touchDragOutside コントロール外で指がドラッグされた touchDragEnter コントロール内へ指がドラッグされた touchDragExit コントロール内から外へドラッグされた touchUpInside コントロール内でタッチアップされた touchUpOutside コントロール外でタッチアップされた touchCancel 不明 valueChanged 値の変化 primaryActionTriggered タッチアップと同じ? editingDidBegin TextFieldでの編集開始 editingChanged TextField内の文字変化や入力 editingDidEnd 画面外タップでのTextFieldの編集終了 editingDidEndOnExit リターンキーによるTextFieldの編集終了 allTouchEvents すべてのタッチイベント allEditingEvents TextFieldに関するイベント applicationReserved 予備だと思う systemReserved 予備だと思う allEvents 全てのイベント 参考
- 投稿日:2019-09-27T07:41:04+09:00
Xcode11で作成したプロジェクトのDeployment Targetを下げるとブラックアウトする問題の対処法
Xcode11とUISceneとUIWindowにまつわるトラブルシュートに追われている。
個別のケースに分解してお届け。現象
Xcode11で作成したプロジェクトをiOS13未満の環境で実行するとブラックアウトしてしまう。
再現手順
- Xcode11にて、新規プロジェクトの作成を行う。
- test off
- SwiftUI off
- Project設定を開き、Deployment Targetを iOS12 にする。
- iOS13未満ならなんでもいい
- iOS シミュレーターのうち、iOS12のもので実行する。
- iOS13未満ならなんでもいい
原因
コンソールに「
AppDelegate
にUIWindow *window
が必要だ」と説明されている。2019-09-27 07:34:57.175062+0900 sample-xcode11[52812:454025] [Application] The app delegate must implement the window property if it wants to use a main storyboard file.iOS13を対象としたアプリケーションでは、root windowは SceneDelegateが所有しているので、AppDelegateには必要ないというAppleのおせっかいである。
対応
AppDelegate
にwindow
を生やす。// AppDelegate.swift @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? }// AppDelegate.h @interface AppDelegate: UIResponder <UIApplicationDelegate> @property (nonatomic, strong) UIWindow *window; @end
- 投稿日:2019-09-27T01:31:30+09:00
【iOS】Sign In with Apple 実装してみた。
Sign in with Appleの必須化
2019/6/3に更新されたレビューガイドラインに以下の記述がありました。
Sign in with Apple will be available for beta testing this summer. It will be required as an option for users in apps that support third-party sign-in when it is commercially available later this year
※ 公式:https://developer.apple.com/news/?id=06032019jどうやらログイン連携にサードパーティを用いているアプリには、Sign in with Apple の実装が必須になるよう…。
これは早めに試してみようということで早速実装してみました!!環境
- Xcode11 以上
- 対応端末iOS13 以上
今回は自社の HiNative を使ってお試し実装してみました。
(ちゃっかり宣伝: WEB・Apple Store)実装
ステップはたったの3つ!結論からいうと超簡単でした。
- Xcodeの設定
- Providerを生成してリクエスト
- Delegateでその後の処理
1. Xcodeの設定
TARGET > Signing & Capabilities に Sign in with Apple を追加します。
これでXcodeの設定は完了です。
1.2. (ステップには記載なし) 適当にボタン作ってね
ボタンとActionを用意してください〜。
ボタンのレイアウトは公式から指定があるので、こちらの公式を参考に!2. Providerを生成してリクエスト
ViewController.swiftimport AuthenticationServices @objc func authorizationAppleID() { if #available(iOS 13.0, *) { let appleIDProvider = ASAuthorizationAppleIDProvider() let request = appleIDProvider.createRequest() request.requestedScopes = [.fullName, .email] let authorizationController = ASAuthorizationController(authorizationRequests: [request]) authorizationController.delegate = self authorizationController.performRequests() } }
if #available(iOS 13.0, *)
なので、ボタンの表示非表示も気をつけないといけないですね。3. Delegateでその後の処理
ViewController.swiftextension ViewController: ASAuthorizationControllerDelegate { @available(iOS 13.0, *) func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential { // 取得できる値 let userIdentifier = appleIDCredential.user let fullName = appleIDCredential.fullName let email = appleIDCredential.email } } @available(iOS 13.0, *) func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) { // エラー処理 } }補足
ASAuthorizationAppleIDCredential
は 2 で生成したASAuthorizationAppleIDProvider
のリクエストが成功した際に呼ばれます。
今回はこの実装のみですが、他にも iCloud Keychainのクレデンシャル情報 を用いた実装もあるようです。
(詳しく知りたい方は下記の参考記事を見てみてください!)動かすとこんな画面が出てきます
FaceIDでログインできるので本当に楽…!!
参考にさせて頂いた記事
- 投稿日:2019-09-27T00:28:13+09:00
SwiftUIでWeb APIから取得した結果を表示する
SwiftUIのViewを更新するためにはデータに応じて、適切な状態の宣言をする必要があります。
ここでは、Web APIと通信し結果を表示するという、よくあるアプリをSwiftUIで実装することで、その方法を紹介します。
必要な実装は以下の2つです。
ObservableObject
に準拠するクラスを作成し変更の通知を発行- SwiftUIのViewで通知を監視
それでは、GitHubのフォローしているユーザー一覧を表示するアプリを例にし、実装していきます。
ObservableObjectに準拠するクラスを作成し変更の通知を発行
通信を行い取得したユーザーを保持するクラスを作成し、
ObservableObject
に準拠させます。(準拠させると言ってもクラスであれば特に何かを実装する必要は無いです)
そして、保持したユーザーの変更を通知するためには、@Published
属性をプロパティに付与します。import Foundation import Combine class FollowingUserStore: ObservableObject { @Published var users: [User] = [] init() { load() } func load() { let url = URL(string: "https://api.github.com/users/maoyama/following")! URLSession.shared.dataTask(with: url) { data, response, error in DispatchQueue.main.async { self.users = try! JSONDecoder().decode([User].self, from: data!) } }.resume() } } struct User: Decodable, Identifiable { var id: Int var login: String }たったこれだけの実装で、通信を行いユーザーを取得できた際の通知を発行することが可能になります。
SwiftUIのViewで通知を監視
今回のような、あるViewだけで使用するデータの通知を監視すれば良い場合はProperty Wrapperの
ObservedObject
を使う方法が適切です。
ObservableObject
をプロパティに保持し、@ObservedObject
を付与します。import SwiftUI struct ContentView: View { @ObservedObject var store = FollowingUserStore() var body: some View { List(store.users) { (user) in UserRow(user: user) } } } struct UserRow: View { var user: User var body: some View { Text(user.login) } }これで
FollowingUserStore.users
の変更を監視できます。まとめ
これだけの実装でWeb APIから取得した結果を表示することができました。
また、適切な宣言をするだけで、データの同期を手動ですることなく、SwiftUIとCombineが自動で行ってくれています。これにより、コードがとてもシンプルで読みやすくなることが期待できそうです。
参考資料
https://developer.apple.com/documentation/swiftui/state_and_data_flow
https://developer.apple.com/videos/play/wwdc2019/226/
https://developer.apple.com/documentation/combine/observableobject
- 投稿日:2019-09-27T00:06:04+09:00
【Swift入門】オプショナル整数型の「?」や「!」について、整理してみた
1. コードを接続した際に自動的に追加されたコードを一つずつ紐解く
ViewController.swift// ---(略)--- @IBOutlet weak var label: UILabel! // ---(略)---1-1.
@IBOutlet
@IBOutlet
は、変数labelがプロパティとしてストーリーボードと接続できることを表している。1-2.
weak
weak
は、メモリ管理の設定。1-3.
var label
var label
は、varキーワードを用いて、変数labelを宣言している。1-4. 「:」の後の
UILabel
「:」の後の
UILabel
は、変数labelの型がUILabelクラスであることを表す。1-5.
!
キーワードは「オプショナル」。2. につづく。
2. オプショナル整数型(= nilを取り得る整数型)について
2-1. オプショナル整数型とは?
nilを変数に代入する場合は、下記のように型を明示することが必要。そして、型の後に「?」を付ける。
examplevar age:Int? = nilこの
Int?
は、オプショナル整数型(= nilを取り得る整数型)と呼ばれる。2-2. オプショナル整数型を使った計算
2-2-1. 問題点
// オプショナル整数型と整数型は型が異なるため、エラーが返る var age:Int? = 25 print(age + 1)↓↓↓
2-2-2. 解決策
変数名の後に「!」を書く。このように指定することで、オプショナル整数型の値が格納されている変数の中から、整数値のみを取り出すことができる。
// 「オプショナル整数型」の変数から整数を取り出す var age:Int? = 25 print(age! + 1)2-2-3. 注意点
変数ageにnilが代入されているときに「!」をつけると、アプリがクラッシュするので注意。
// nilが代入されている変数から値を取り出すと、アプリがクラッシュ var age:Int? = nil print(age! + 1)★ つまり
オプショナル型の変数から値を取り出すときに「!」を付けることで、変数の中身がnilでないことを宣言していることになる。
3. オプショナル整数型のメリット:きちんとエラーが表示されるようにして、謎のクラッシュ防ぐ
3-1.NG例
Swiftのチェックは通るは、アプリがクラッシュするvar age:Int = nil print(age + 1)3-2. OK例
型が異なるため、Swiftのチェックできちんとエラーが表示されるvar age:Int? = nil print(age + 1)4. 「!」を省略した書き方
4-1. 省略前
「!」を省略する前var age:Int? = nil print(age! + 1)4-2. 省略後
「!」を省略したあとvar age:Int! = nil print(age + 1)=>
4-1.
と4-2.
は、同じ働きをするが、4-2.
の方がシンプルに書くことができる。5. まとめ
ViewController.swift// ---(略)--- @IBOutlet weak var label: UILabel! // ---(略)---つまり、下記の
UILabel!
は、labelプロパティから値を取り出すときに「!」を書くのを省略するための書き方。labelプロパティは、これ以降、プロパティのあとに「!」を付けることなく値を取り出すことができる。
- 投稿日:2019-09-27T00:03:17+09:00
【Swift】Alamofire, MoyaのURLEncodingで、Boolが0,1になってしまうことへの対応
事象: Moyaを使ってAPIをリクエストしたが、Boolが0/1で送られていた
API系のライブラリとしてよく用いられるのが、 Moya というライブラリだと思います。
私自身も99%のプロジェクトでMoyaを使っています。
Moyaでリクエストパラメータを送る際に以下の形式で送っています。
requestParameters(parameters: [String : Any], encoding: Moya.ParameterEncoding)しかしながら、この形式で送ると、Bool型が一度Any型に変換され、そして、最終的にNSNumberとしてリクエストされるようです。
詳しくは こちら
とはいえ、↑この人が言うように、BoolをAnyにすると、NSNumberになる的な事象は、playground上では確認できませんでした。。
対応: Bool.description で、Bool型をString型に変更して対応
Bool型→Any型: NSNumber (0/1)
Bool型→String型→Any型: Bool (true/false)↑のようになるようです。
元々のコード
var parameters: [String: Any]? { return ["key" : boolParam] }↓↓↓↓↓↓↓↓↓
修正後のコード
var parameters: [String: Any]? { return ["key" : boolParam.description] }その他
パケットキャプチャで確認しましたが、こちら↓とても参考になりました。
通信系のデバッグには Charles が便利