More Related Content Similar to Keep a diary (20) Keep a diary5. 振り返り: 2016/11
• 2016/11/14 Cocoa Study at Ikebukuro #5
• Bluemix再挑戦
• Project Linkingを試す
• 2016/11/19 Cocoa Study at Matudo #2
• ProxyResponder nibファイルを超えたTarget/action
• シェアウェア、やって良い事、悪い事
• 2016/11/25 第5回 MOSA自習室
• Swiftyを試す
• SwiftからGUI AppをコントロールするScriptarian
• PlantUMLのsalt言語
6. 振り返り: 2016/12
• 2016/12/5 Cocoa Study at Ikebukuro #6
• Swiftyを試す
• GNUstep用アプリ”MyWiki.app”をビルドする
• MacOSX用アプリ”CardBook.app”をビルドする
• TouchBarDemoApp
• MindTo01sサイトの紹介
7. 振り返り: 2017/1
• 2017/1/16 Cocoa Study at Ikebukuro #7
• Swiftyを試す
• iOS上のQRコードリーダーアプリのメールフィールド認識についての調査
• ジェスチャーやカスタムトランジションを利用して入力時やコンテンツ表示時に一工夫を加え
たUIの実装ポイントまとめ
• 2017/1/28 Cocoa Study at Matudo #3
• 単車の手帳
• macOS Cocoaを始める
• アイデア出しソフトウエア
• NSAtomicStoreサブクラスの作成
8. 振り返り: 2017/2
• 2017/2/6 Cocoa Study at Ikebukuro #8
• ピラミッド原則のアプリ化について
• 2017/2/18 Cocoa Study at Matudo #4
• 2017/2/24 第6回 MOSA自習室
• スパゲッティ紀行
• 私の開発環境
• 効果的な学習方法について
• Hide scroll bar from PDFView
9. 振り返り: 2017/3
• 2017/3/6 Cocoa Study at Ikebukuro #9
• Document-Based Application
• storyboardの中のNSWindow initialFirstResponderが効かない問題について
• Cocoaにおける状態の保存と復元その2
• 2017/3/17 MOSA BUKURO.swift #7
• 私の営業の失敗談
• 継承vs使用
• [macOS]Document-based Application
• pragma markによるメソッドの分類
• 2017/3/25 Cocoa Study at Matudo #5
• 最小限アウトラインプロセッサの作り方
10. 振り返り: 2017/4
• 2017/4/3 Cocoa Study at Ikebukuro #10
• swiftによるアプリケーション開発-入門編
• [macOS]ヘルパ・オブジェクト
• Apple File System の Frequently Asked Questions
• Cocoaからのライブ変換(LiveConversion)の変更と監視
• 2017/4/14 MOSA BUKURO.swift #8
• swiftによるアプリケーション開発-入門編
• uncrustifyもういっぱい
• Doxygenでpdf出力
11. 振り返り: 2017/5
• 2017/5/8 Cocoa Study at Ikebukuro #11
• swiftによるアプリケーション開発-入門編
• Playing with RESTful APIs
• 2017/5/15 AKIBA.swift x BUKURO.swift #9 合同勉強会
• キーチェーン・アクセスのバックアップと同期
• Macアプリのインストーラ作成ツールSwift版
• cocoaで作る最小限カード型データベースの実装
• iOS macOS クロスプラットフォーム
• CGAffineTransform 実践入門
• 2017/5/27 Cocoa Study at Matudo #6
• 最小限データーベースその2
• 最小限データーベースその3
12. 振り返り: 2017/6
• 2017/6/5 Cocoa勉強会 BUKURO.swift #12
• 2017/6/16 MOSA BUKURO.swift #10 【WWDC2017特集!】
• WWDC2017
• WWDC2017レポート
• Wikipediaを用いた検索キーワードのカテゴリ連想拡張
• 渦巻き
• 2017/6/24 関東swift勉強会2017-6
• swiftによるアプリケーション開発-入門編
• 計算機プログラムの構造と解釈(SICP)
• swiftでハノイの塔
• swiftで三段論法
• swiftで最小限タートルグラフィック
13. 振り返り: 2017/7
• 2017/7/3 Cocoa勉強会 BUKURO.swift #13
• Machスレッドスタック
• Swiftで8人の女王
• swiftでアナグラム
• 2017/7/14 MOSA BUKURO.swift #11
• swiftによるアプリケーション開発-入門編
• 人工知能を実装する 意味ネットワーク
• 2017/7/22 関東swift勉強会2017-7
• swiftで迷路の脱出
• swiftで迷路の作成
• PlaygroundでStoryboardの試作
• 空配列
14. 振り返り: 2017/9
• 2017/9/4 Cocoa勉強会 BUKURO.swift #14
• Swiftによるアプリケーション開発:入門編
• AS.Parallel
• Cross-platformプロジェクト
• Objective-Cで書いていたライブラリをSwiftへ移植中に得られた知見など色々
• [macOS]Day One Classic 書類
• 2017/9/13 MOSA BUKURO.swift #12
• Swiftによるアプリケーション開発:入門編
• Designing for iPhone X
• Building Apps for iPhone X
• safeAreaLayoutGuide
• 2017/9/23 関東swift勉強会2017-9
• Control Enablerのswiftへの移植
• NSMenuItemのショートカットキー表記方式の変更
• Apple Technology Lab. (よろず相談室)
16. How does Apple File System handle filenames?
APFS accepts only valid UTF-8 encoded filenames for creation, and preserves both case and normalization of the filename on disk in all variants. APFS,
like HFS+, is case-sensitive on iOS and is available in case-sensitive and case-insensitive variants on macOS, with case-insensitive being the default.
In macOS High Sierra, APFS is normalization-insensitive in both the case-insensitive and case-sensitive variants, using a hash-based native normalization
scheme. In iOS 11, APFS is normalization-insensitive as well, using either a native normalization scheme (erase restores only) or runtime normalization
scheme (upgrades from previous versions). Runtime normalization will also be available in iOS 10.3.3 and macOS Sierra 10.12.6. Being normalization-
insensitive ensures that normalization variants of a filename cannot be created in the same directory, and that a filename can be found with any of its
normalization variants. This means that developers don’t need to do any additional work to ensure correct normalization behavior in these versions of
macOS and iOS.
Some differences between how APFS and HFS+ handle filenames include the following:
• APFS implements normalization and case insensitivity according to the Unicode 9.0 standard; this enables APFS to support a wider range
of languages for these features than HFS+, which is based on Unicode 3.2.
• APFS preserves the normalization of the filename and uses hashes of the normalized form of the filename to provide normalization
insensitivity, whereas HFS+ stores the normalized form of the filename on disk to provide normalization insensitivity.
• Calling readdir(2) on a directory in APFS returns filenames in hash order, whereas HFS+ returns filenames in lexicographical order.
• While both filesystems expect filenames to be encoded in UTF-8, APFS stores filenames on disk in UTF-8 encoding, whereas HFS stores
filenames on disk in UTF-16 encoding.
• APFS doesn’t allow files to be created with filenames that contain unassigned codepoints in the Unicode 9.0 standard, whereas HFS+ does.
In iOS 10.3 and in the case-sensitive variant of the developer preview of APFS in macOS Sierra, APFS is normalization-sensitive. For these versions,
developers should be aware of behavior differences between normalization sensitivity and insensitivity that may arise when a device upgrades macOS or
iOS and migrates the filesystem from HFS+ to APFS. For example, attempting to create a file using one normalization behavior and then opening that file
using another normalization behavior may result in ENOENT, or “File Not Found” errors. Additionally, storing filenames externally, such as in the defaults
database, Core Data, or iCloud storage may cause problems if the normalization scheme of the filename being stored is different from what exists on disk.
To avoid introducing bugs in your code with mismatched Unicode normalization (for iOS 10.3.0, 10.3.1 and 10.3.2) in filenames, do the following:
• Use high-level Foundation APIs such as NSFileManager and NSURL when interacting with the filesystem.
• Use the fileSystemRepresentation property of NSURL objects when creating and opening files with lower-level filesystem APIs such
as POSIX open(2), or when storing filenames externally from the filesystem.
17. 予定
• 2017/10/20 Swift勉強会 Developer’s Tech. Lab.
• アウトライプロセッサ進捗会議
• お空に日記を預ける
• Developer's Tech Lab
• iOSDC 2017の参加報告
• Cocoa勉強会 MOSA 合同勉強会
BUKURO.swift 2017-11
18. keep a diary
• 発端
• どう実装するか?
• Day Oneのデータ形式を探る
• CloudKit
• Cross-platform プロジェクト
• macOSアプリケーション
• keep a diary
20. • Day Oneという日記アプリを利用しています。
• 種類が増えて、今後が不安。
• Day One Classic
売り切り。発表者は、これのユーザ。新規購入不可。
• Day One 2.0
別アプリとしてリリース。新規購入不可。
• Day One Premium/Plus/Basic
定額制プラン。
21. Sync
• Day One
独自サーバ。
• iCloud
探せばデータを見つけることは出来るが、見えないと考
えるべきか。
~/Library/Mobile Documents/*~com~dayoneapp~dayone/Documents/Journal_dayone
• Dropbox
データは見える場所。
~/Dropbox/アプリ/Day One/Journal.dayone
23. Day One
+- Journal.dayone
+- entries
| 1A573C0B559248A991C4AC0F0EA8B1E7.doentry
| 1FB23D983DF341079532B0F9381BCFCC.doentry
| :
+- photos
1A573C0B559248A991C4AC0F0EA8B1E7.jpg
2D37E853754E43EFBA0FFBAEEB6A8B40.jpg
:
24. <?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>Activity</key>
<string>Automotive</string>
<key>Creation Date</key>
<date>2016-07-18T08:51:34Z</date>
<key>Creator</key>
<dict>
<key>Device Agent</key>
<string>iPhone/iPhone7,2</string>
<key>Generation Date</key>
<date>2016-07-18T08:51:34Z</date>
<key>Host Name</key>
<string>iPhone6GB128Gold</string>
<key>OS Agent</key>
<string>iOS/9.3.2</string>
<key>Software Agent</key>
<string>Day One iOS/1.17.9</string>
</dict>
<key>Entry Text</key>
<string>AT教習のゼッケン
下で待つ。準備なし</string>
:
31. @IBAction func performDumpDayOneClassic(sender: AnyObject) {
let openPanel = NSOpenPanel()
openPanel.allowsMultipleSelection = false
openPanel.canChooseDirectories = true
openPanel.canCreateDirectories = false
openPanel.canChooseFiles = false
openPanel.begin { (result) -> Void in
if result == NSFileHandlingPanelOKButton {
let inst = KeepADiary()
inst.dump(url: openPanel.urls[0])
}
}
}
32. class KeepADiary {
public func dump(url: URL) -> Void {
print(url)
var entriesUrl = url
entriesUrl.appendPathComponent("entries", isDirectory: true)
print(entriesUrl)
var fileList: [String] {
do {
return try FileManager.default.contentsOfDirectory(atPath: entriesUrl.path)
} catch {
return []
}
}
for fileName in fileList {
var fileUrl = entriesUrl
fileUrl.appendPathComponent(fileName)
let plistXML: NSData = FileManager.default.contents(atPath: fileUrl.path)!
as NSData
let temp = try! PropertyListSerialization.propertyList(from:plistXML as Data,
options: [], format: nil) as! [String:Any]
print(temp)
}
}
}
33. ["Entry Text": AT教習のゼッケン
下で待つ。準備なし, "UUID": 38906447EE98439F9854DA354D9E95FA, "Step Count": 6909, "Creator": {
"Device Agent" = "iPhone/iPhone7,2";
"Generation Date" = "2016-07-18 08:51:34 +0000";
"Host Name" = iPhone6GB128Gold;
"OS Agent" = "iOS/9.3.2";
"Software Agent" = "Day One iOS/1.17.9";
}, "Ignore Step Count": 1, "Location": {
"Administrative Area" = "U57fcU7389U770c";
Country = "U65e5U672c";
Latitude = "35.79399523328598";
Locality = "U548cU5149U5e02";
Longitude = "139.637718340512";
"Place Name" = "U4e0bU65b0U50095U4e01U76ee27U756a1U53f7";
Region = {
Center = {
Latitude = "35.79412019872211";
Longitude = "139.6382801";
};
Radius = "70.86956812177714";
};
}, "Starred": 0, "Time Zone": Asia/Tokyo, "Activity": Automotive, "Weather": {
Celsius = 32;
Description = "Partly Cloudy";
Fahrenheit = 90;
IconName = "fair.png";
"Pressure MB" = 1005;
"Relative Humidity" = 55;
Service = HAMweather;
"Sunrise Date" = "2016-07-17 19:39:47 +0000";
"Sunset Date" = "2016-07-18 09:56:36 +0000";
"Wind Bearing" = 180;
"Wind Chill Celsius" = 32;
"Wind Speed KPH" = 15;
}, "Creation Date": 2016-07-18 08:51:34 +0000]
36. どれくらい使える?
• 10 GB Asset storage
• 100 MB Database storage
• 2 GB Data transfer
• 40 Requests per second
• Overage Fees
• Asset storage $0.03/GB
• Database storage $3.00/GB
• Data transfer $0.10/GB
• Requests per sec $100 per 10 requests
40. フィールド型 クラス Description
Asset CKAsset
大容量ファイルをレコードに
関連づけて別個に保存
Bytes NSData
バイトバッファをレコードに
保存するためのラッパー
Date/Time NSDate 時間軸上のある一時点
Double NSNumber 倍精度浮動小数点数
Int(64) NSNumber 整数
Location CLLocation
地理的座標(経緯度)および
高度
Reference CKReference
あるオブジェクトから別のオ
ブジェクトを参照するリレー
ション
String NSString
書き換え不可のテキスト文字
列
List NSArray
上記いずれかのフィールド型
の配列
53. コードで設定
class ViewController: NSViewController {
override func awakeFromNib() {
self.view.window?.initialFirstResponder = self.nameField
}
}
この問題、BUKURO.swiftで議論した際に、汎用的な方法の提案もありましたが、
複雑だったので、この方法がベターかなと思っています。
http://www.mindto01s.com/2017/02/26/e97ab989_f357_44c7_8810_21ce715d9d2b.html
いい方法をご存知な方がいらっしゃいましたら、是非。
58. アカウントの確認
CKContainer.default().accountStatus(completionHandler: {
accountStatus, error in
if error != nil {
print(error ?? "")
}
else if accountStatus == .noAccount {
let alert = UIAlertController.init(title: "Sign in to iCloud",
message: "Sign in to your iCloud account to write records.On the Home screen,
launch Settings, tap iCloud, and enter your Apple ID.Turn iCloud Drive on.If you don't have an iCloud
account, tap Create a new Apple ID.",
preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Okay", style: .cancel, handler: nil))
self.present(alert, animated: true, completion: nil)
}
else {
print(“Insert your just-in-time schema code here”)
}
})
62. 追加
/* レコードIDを生成 */
let uuidString = UUID.init().uuidString
let diaryRecordID: CKRecordID = CKRecordID(recordName: uuidString)
/* レコード・オブジェクトを生成 */
let diaryRecord: CKRecord = CKRecord(recordType: "Diary", recordID: diaryRecordID)
/* レコードのフィールドを設定 */
diaryRecord["UUID"] = uuidString as CKRecordValue
diaryRecord.setObject("本文です。" as NSString, forKey: "EntryText")
diaryRecord.setObject(Date() as NSDate, forKey: "DiaryDate")
/* レコードを保存する */
database.save(diaryRecord, completionHandler: { (record, error) in
if error != nil {
print(error)
}
else {
print(record)
}
})
63. 更新
/* レコードIDからレコードを取得する */
database.fetch(withRecordID: diaryRecordID, completionHandler: { (record, error) in
if error != nil {
print(error)
}
else {
/* 値を更新する */
record.setObject("本文を更新します。" as NSString, forKey: "EntryText")
/* レコードを保存する */
database.save(diaryRecord, completionHandler: { (record, error) in
if error != nil {
print(error)
}
else {
print(record)
}
})
}
})
65. 検索
/* 検索条件 */
let now = Date()
var calendar = Calendar.current
calendar.locale = Locale(identifier: "ja")
var component = calendar.dateComponents([.year, .month, .day], from: now)
component.hour = 0
component.minute = 0
component.second = 0
let beginDate: NSDate = calendar.date(from:component)! as NSDate
let endDate: NSDate = NSDate(timeInterval: (60.0 * 60.0 * 24.0), since: beginDate as Date)
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "ja_JP")
dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss"
let beginDateString = dateFormatter.string(from: beginDate as Date)
let endDateString = dateFormatter.string(from: endDate as Date)
let predicate = NSPredicate(format: "DiaryDate != %@", NSDate())
let query : CKQuery = CKQuery(recordType: "Diary", predicate: predicate)
/* 検索する */
database.perform(query, inZoneWith: nil, completionHandler: {
results, error in
if error != nil {
print(error)
}
else {
print(results)
/* 検索した結果 */
for record in results! {
/* 削除する */
database.delete(withRecordID: record.recordID, completionHandler: { (recordId, error) in
if error != nil {
print(error)
}
else {
print(recordId)
}
})
}
}
})
68. サブスクリプション保存
/* 条件 */
let predicate = NSPredicate(format: "TRUEPREDICATE")
let subscription = CKSubscription(recordType: "Diary",
predicate: predicate,
options: [CKSubscriptionOptions.firesOnRecordCreation,
CKSubscriptionOptions.firesOnRecordUpdate])
/* 通知 */
subscription.notificationInfo = CKNotificationInfo()
subscription.notificationInfo?.alertBody = "Item has been added."
subscription.notificationInfo?.alertLocalizationKey = "Diary record has changed!"
subscription.notificationInfo?.shouldBadge = true
/* 保存 */
database.save(subscription, completionHandler: {
subscription, error in
if error != nil {
print(error)
}
else {
print(subscription)
}
})
70. macOS
func applicationDidFinishLaunching(_ aNotification: Notification) {
NSApplication.shared().registerForRemoteNotifications(matching:
[.alert, .sound])
}
func application(_ application: NSApplication,
didRegisterForRemoteNotificationsWithDeviceToken
deviceToken: Data) {
}
func application(_ application: NSApplication,
didFailToRegisterForRemoteNotificationsWithError
error: Error) {
}
func application(_ application: NSApplication,
didReceiveRemoteNotification userInfo:
[String : Any]) {
}
71. iOS
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions:
[UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if #available(iOS 10.0, *) {
let center = UNUserNotificationCenter.current()
center.delegate = self
center.requestAuthorization(options: [.alert, .sound]) { (granted, error) in
if error != nil {
return
}
if granted {
DispatchQueue.main.async {
application.registerForRemoteNotifications()
}
} else {
}
}
}
else {
let settings = UIUserNotificationSettings(types: UIUserNotificationType.alert, categories:nil)
application.registerUserNotificationSettings(settings)
application.registerForRemoteNotifications()
}
return true
}
72. func application(_ application: UIApplication, didRegister notificationSettings: UIUserNotificationSettings) {
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
}
func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
let cloudKitNotification: CKNotification = CKNotification(fromRemoteNotificationDictionary: userInfo)
let alertBody = cloudKitNotification.alertBody
print("alertBody: (String(describing: alertBody))")
if cloudKitNotification.notificationType == .query {
let queryNotification = cloudKitNotification as! CKQueryNotification
let recordID = queryNotification.recordID
}
}
func application(application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
}
/* 前面で通知受信 */
@available(iOS 10.0, *)
func userNotificationCenter(_ center: UNUserNotificationCenter,
willPresent notification: UNNotification,
withCompletionHandler completionHandler:
@escaping (UNNotificationPresentationOptions) -> Void) {
}
/* 通知を選択 */
@available(iOS 10.0, *)
func userNotificationCenter(_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
completionHandler()
}
Editor's Notes 勉強会の意義。
発表することによって理解が深まる。
他人の成果を吸収する。
コミュティづくり。営業の側面もある。
アクティブメンバーの有志で非公開グループが運営されている。
待ってます。 申し込みは六十数名。
三十数名キャンセル。 十数名。 Day One Classicの場合。
三つの同期方法を切り替えてゆくと、三つとも同じ内容になった。
Dropbox上のデータが移行に使えると判断。 UUIDがエンティティのファイル名。画像は別ファイルでUUIDがファイル名。
全体を管理するファイルは用意していない。多分、起動時になめて、動的な目次を持っている? 文書の書式はプロパティリスト。 *ローカルに保持
**ユーザに見せても、見せられた方も困る。
**iCloudのバックアップで同期できる。
*独自サーバ
**サーバとそこで動くプログラムが必要。
*クラウドサービス
**iCloud 特別な契約は不要。
***iCloud Drive
****ユーザにファイルを見せても、見せられた方も困る。
***CloudKit
****異なるOSや端末で同期できる。
**Firebase
Googleの方針変更が怖い
***Firebase Realtime Database
***Cloud Storage 日記用途だと、100MBは辛い? データはコンテナに格納される。
別の開発者のコンテナにはアクセスできない。
自分が製作したアプリで、自分のコンテナを共有することはできる。
CloudKitを有効にすると、バンドルIDと同じ名前のコンテナが生成される。
一度生成したコンテナは削除できない。
macOSとiOSで同じバンドルIDを設定することができない。 SpriteKitの雛形がCross-platformなので、これの構成を参考にした。 空のプロジェクトを生成する。 macOSのCocoa Applicationをターゲットとして追加する。 ターゲット名をmacOSとするのがオススメ。
例えば、プロダクト名のKeepADiaryを設定すると、iOSターゲットとかぶる。
また、KeepADiaryForMacOSとすると、長くなってしまい、操作に支障が出る。 CapabilitiesでCloudKitを有効にすると、このバンドルIDでコンテナが生成される。
生成されたコンテナは削除できないので、じっくりと検討して、バンドルIDを決めること。 同じファイル名にすると、Xcodeは正しく使い分けてくれない。 サンプルのコードを見ていきます。 公開データベースの内容は、CloudKit Dashboardで確認でき編集できるが、非公開データベースだとできないので、最初は公開データベースで試行錯誤するのがオススメ。 非公開データベースは、ユーザー毎。 iCloudアカウントでサインイン。 試験用アカウントのデータは、試験用アカウントでないとアクセスできないので、そのApple IDを開発アカウントにして、ダッシュボードで表示。 実稼働環境で利用されるようになると、スキーマは変更できない。 エンティティの、あるフィールドの値変更は、そのエンティティ全体の更新となる。 ここで試行錯誤できる。 データベースに対して保存ということなので、上書き時はエラーのようです。 通知を駆使すれば、常に同じ値を保つこともできるかもしれないが、通知は必ず届くものでないのと、リソースの制限があることを考えると、同期ボタンを押すと、お空にデータを預ける、そして、同期されるという利用がいいのかも。