- 投稿日:2020-10-14T23:16:04+09:00
[Unity]Core Hapticsを使ってiOSでバイブレーションを自由に動作させる
単純なバイブレーションの実装
Unityでは
Handheld.Vibrate ();というメソッドが用意されていて、iOSでもAndroidでも振動させるだけならこれ一つで実現できます。
が、これは ただ1秒くらい「ブーン」となる以外に何も調整ができず 、UXや演出として利用するには流石に "わびさび" が足りません。ちょっと凝ったバイブレーションの実装
ちょっとだけ手間ですが、iOS、Androidのネイティブ処理を呼び出すことで少し融通の利くバイブレーションを実装できます。下記の記事が分かりやすくまとまっていて、参考になります!
Androidは端末のバリエーションが多すぎる故か、これで 振動時間を調整するのが限界 ですが、一応Unityに用意されたメソッドを呼ぶよりはマシなバイブレーションが実装できるはずです。
iOSでは、
AudioServicesPlaySystemSound
というネイティブのメソッドを使うことで、 OSに用意されたサウンドID を渡し、その中で音無し+バイブレーション有りのパターンのものを使うことができます。ざっと確認した感じ、使えそうなのは以下のサウンドIDでした。
1003:ブッ(ごく小さな音が入っているが、ほぼ聞こえず) 1011:ブッブー 1102:ププッ 1161:カリッ 1162:カリカリッ 1164:カリカリカリ... 1311:ブッブー 1350:ブーッ 1519:プッ 1521:プルッ通知やUX用に用意されたパターンの振動なので、それなりに使い勝手の良いバイブレーションが揃っています。 めちゃくちゃこだわるようなことがなければこの中から選べばよい と思います。
が、あくまでパターンの中から選ぶ形なので、振動時間は調整できないし、縛りが強いと言わざるをえません。しかし、 Haptics を使えばかなり細かい振動を実装できます。
Haptic Feedback(触覚フィードバック)
iPhoneは、iPhone7以降、ホームボタンが物理ボタンではなくなりました。(2020 10/16 追記:嘘でした。バリバリ物理ボタンありましたw)
そこで生まれたのが 「Haptic Feedback」 と呼ばれる、 タッチディスプレイ上でもボタンを押したようなフィードバックを得られるバイブレーション の仕組みです。
iPhone7以降の端末では、画面の下から上にフリックすることでホーム画面へ戻ることができますが、その時に「グッ」というような振動が起きる、あれです。そして iOS13 以降、この仕組みを自由に使える Core Haptics というAPIが追加されました。
これを使えば、振動時間だけでなく、 振動の「強さ」や「鋭さ」 というような、Haptic Feedback で使われていたかなり細かなバイブレーションのパラメータを調整することができます(この辺はOSも端末も一社で作ってるAppleにしかできない仕組みですね)。
生まれた経緯ゆえ、 かなりUXの強化に向いた機能 になっています!ということで、本記事ではそれをUnityから使うためのアレコレをまとめます。
ネイティブ実装は普段頻繁にやるようなものでもないし、Objective-C や Swift を書くことになるので、思いのほか手こずりました(というか徹夜するハメになりました)。
この記事でこれから Haptics を使う人が徹夜しなくて済むようになりますように。。。■余談 - Macbook の Haptics
最近のMacbookではトラックパッドを押したときに「カチッ」という感触がありますが、実はあれも Haptic Feedback と同じ仕組みを使ってるらしいです。実際には押し込めるような構造になってないので電源を消してトラックパッドを押しても何にも感触がありません。(自分もHaptics を調べてるときに知ってクッソ驚きました)
検証環境
本記事は以下のバージョンで動作を確認してます。
- Unity 2018.4.11f1
- Xcode 11.1
ファイルの作成
Core Haptics はiOSネイティブで提供されているAPIで、Objective-C でも Swift でも呼び出せるようになっていますが、今回は Swift での実装方法 を紹介します。
上図のように、Unityから Swift の処理を呼ぶには iOSプラグインを経由する必要があります。
以下のフォルダ構造で、それぞれファイルを作成してください。Assets ┣ VibrationUtil.cs ┗ Plugins ┣ VibrationObjc.mm ┗ iOS ┗ VibrationSwift.swift■余談 - なんでSwift?
Objective-C で処理を書けば、iOSプラグインファイルで直接 Core Haptics API が呼べます。
普段 Objective-C はおろか、Swift も書かない僕はその方法で済ませたかったですが、公式ドキュメントの実装例 が なぜか Swiftの分しか無かったので、今回は泣く泣くSwiftを選ぶことにしました。SwiftでCore Hapticsの処理を記述
ファイル全文
VibrationSwift.swiftimport Foundation import CoreHaptics @objc public class VibrationSwift : NSObject { @available(iOS 13.0, *) @objc public static var hapticEngine: CHHapticEngine? @available(iOS 13.0, *) @objc public static func setupHapticEngine() { do { hapticEngine = try CHHapticEngine() try hapticEngine?.start() } catch { print("Failed to restart the engine") } } @available(iOS 13.0, *) @objc public static func playHapticEngine(intensityValue: Float, sharpnessValue: Float, durationValue: Float, sustainedValue: Float) { if hapticEngine == nil { setupHapticEngine() } let intensity = CHHapticEventParameter(parameterID: .hapticIntensity, value: intensityValue) let sharpness = CHHapticEventParameter(parameterID: .hapticSharpness, value: sharpnessValue) let sustained = CHHapticEventParameter(parameterID: .sustained, value: sustainedValue) let event = CHHapticEvent(eventType: .hapticContinuous, parameters: [intensity, sharpness, sustained], relativeTime: 0, duration: TimeInterval(durationValue)) do { let hapticPattern = try CHHapticPattern(events: [event], parameters: []) let hapticPlayer = try hapticEngine?.makePlayer(with: hapticPattern) try hapticPlayer?.start(atTime: 0) } catch { print("Failed to play") } } }解説
import CoreHapticsまず、必ず CoreHaptics をインポートしてください。
@objc public static var hapticEngine: CHHapticEngine?CoreHaptics による振動を管理する
CHHapticEngine
の変数です。static にして、static メソッドから参照できるようにしてください。
@objc public static func setupHapticEngine()変数定義した
hapticEngine
の初期化処理です。
@objc public static func playHapticEngine(intensityValue: Float, sharpnessValue: Float, durationValue: Float, sustainedValue: Float)Hapticsによる振動メソッドです。引数はそれぞれ以下のパラメータを表しています。
- intensityValue:振動の強さ(0~1)
- sharpnessValue:振動の鋭さ(0~1)
- durationValue:振動する時間(秒)
- sustainedValue:指定した時間の間、振動させ続けるか否か(0ならfalse、1ならtrue)
durationValue
の時間は、これがtrueでないと無効となりますメソッド内の
CHHapticEventParameter
を使えば、他にもいろんなパラメータを扱えるようですが、ひとまず主要なものはこんなところかなと。■参考 - その他のパラメータ
- attackTime:触覚パターンの強度が増加し始める時間
- decayTime:触覚パターンの強度が減少し始める時間
- releaseTime:触覚パターンのフェードを開始する時間
※ ドキュメント の「Haptic Event Parameter IDs」を参照
let attackTime= CHHapticEventParameter(parameterID: .attackTime, value: 0)というようにパラメータを定義し、
CHHapticEvent
のparameters
に渡すことで設定できます。
上記コードを例にすると、let event = CHHapticEvent(eventType: .hapticContinuous, parameters: [intensity, sharpness, sustained, attackTime], relativeTime: 0, duration: TimeInterval(durationValue))こんな感じです。(
sustained
の後にattackTime
を追加)■参考 - 遅延実行
CHHapticEvent
のrelativeTime
の引数を使うことで、遅延実行をすることもできます。
例えば「ブブッ」というような複数回の振動パターンを作るときに必要になるでしょう。let event = CHHapticEvent(eventType: .hapticContinuous, parameters: [intensity, sharpness, sustained], relativeTime: TimeInterval(1), duration: TimeInterval(durationValue))こんな感じで、
TimeInterval
をかまして時間(秒)を指定してください。
@available(iOS 13.0, *)各変数/メソッドの前に書かれているこの記述ですが、これは Core Haptics が iOS13 から導入されたものであるため、それ以前のバージョンのOSからは呼ばないでね、というやつです。無いとエラーが出ます。
Swiftの処理をiOSプラグインで呼び出し
ファイル全文
VibrationObjc.mm#import <Foundation/Foundation.h> #import <CoreHaptics/CoreHaptics.h> #import <UnityFramework-Swift.h> #ifdef __cplusplus extern "C" { #endif void setupHapticEngine() { [VibrationSwift setupHapticEngine]; } void playHapticEngine(float intensity, float sharpness, float duration, float sustained) { [VibrationSwift playHapticEngineWithIntensityValue:intensity sharpnessValue:sharpness durationValue:duration sustainedValue: sustained]; } #ifdef __cplusplus } #endif解説
#import <UnityFramework-Swift.h>これは、Xcodeのビルド時に自動的に作られるSwiftのヘッダーファイルです。
何もしないとファイル名は 「{プロジェクト名}-Swift.h」 となるのですが、それだと汎用性に欠けるので、後ほどUnityの実装で任意の名前に固定します。※今回は 「UnityFramework-Swift.h」 としました
[VibrationSwift 〇〇〇];
VibrationSwift.swift
で定義したVibrationSwift
クラスの、「〇〇〇」というメソッドを呼ぶ、という記述です。
[VibrationSwift playHapticEngineWithIntensityValue:intensity sharpnessValue:sharpness durationValue:duration sustainedValue: sustained];
VibrationSwift
クラスの「playHapticEngine」というメソッドを呼んでいる…のですが、ここ、Objective-C の独特な文法です。
第1引数を渡すとき、メソッド名に 「With+イニシャルを大文字にした引数名」 という記述をくっつけて、コロン(:)の後に第1引数書いています。第2引数以降は「引数名:引数」で書いていけばOKです。
Objective-Cを知らないが故、ちょっとハマりました。iOSプラグインの処理をUnityで呼び出し
ようやくUnityに辿り着いた!
ファイル全文
VibrationUtil.csusing System.Runtime.InteropServices; using UnityEditor.Callbacks; using System.IO; #if UNITY_IOS using UnityEditor.iOS.Xcode; #endif public class VibrationUtil { #if UNITY_IOS && !UNITY_EDITOR [DllImport("__Internal")] private static extern void setupHapticEngine(); [DllImport("__Internal")] private static extern void playHapticEngine(float intensity, float sharpness, float duration, float sustained); #endif public static void SetupHapticEngine() { #if UNITY_IOS && !UNITY_EDITOR setupHapticEngine(); #endif } public static void PlayHapticEngine(float intensity, float sharpness, float duration) { #if UNITY_IOS && !UNITY_EDITOR float sustained = duration > 0 ? 1 : 0; playHapticEngine(intensity, sharpness, duration, sustained); #endif } [PostProcessBuild(0)] public static void OnPostprocessBuild(BuildTarget target, string path) { string projectPath = PBXProject.GetPBXProjectPath(path); PBXProject pbxProject = new PBXProject(); pbxProject.ReadFromString(File.ReadAllText(projectPath)); string target = pbxProject.TargetGuidByName("Unity-iPhone"); pbxProject.SetBuildProperty(target, "SWIFT_VERSION", "4.2"); pbxProject.SetBuildProperty(target, "SWIFT_OBJC_INTERFACE_HEADER_NAME", "UnityFramework-Swift.h"); pbxProject.AddFrameworkToProject(target, "CoreHaptics.framework", false); File.WriteAllText(projectPath, pbxProject.WriteToString()); } }解説
[DllImport("__Internal")] private static extern void 〇〇〇();iOSプラグインで定義した「〇〇〇」というメソッドをC#(Unity)に定義する、という記述です。
定義すれば、C#内で普通のメソッドのように扱えます。
[PostProcessBuild(0)] public static void OnPostprocessBuild(BuildTarget target, string path)
PostProcessBuild
を付けたメソッドは、UnityのiOSビルドでXcodeプロジェクトが出力された後に呼び出されるようになります。(カッコ内の数字はその順番。小さいほど先に呼ばれる)※ドキュメント
これを利用して、Xcodeプロジェクトでやるべき設定を自動化しています。
pbxProject.SetBuildProperty(target, "SWIFT_VERSION", "4.2");Xcodeで使うSwiftバージョンを指定します。これが無いと、「Swiftバージョンが未指定」という旨のエラーが出るはずです。
"4.2"の部分は、 使用しているXcodeがサポートしてるSwiftバージョンを使ってください 。
pbxProject.SetBuildProperty(target, "SWIFT_OBJC_INTERFACE_HEADER_NAME", "UnityFramework-Swift.h");「Swiftの処理をiOSプラグインで呼び出し」の節(VibrationObjc.mm)で説明した、Swiftのヘッダーファイルの名前を指定しています。
VibrationObjc.mm
で記述するヘッダー名と一致させれば、任意の名前を付けることができます。
pbxProject.AddFrameworkToProject(target, "CoreHaptics.framework", false);CoreHapticsのフレームワークをXcodeに追加しています。
使用例
VibrationTest.csusing UnityEngine; public class VibrationTest : MonoBehaviour { void Start() { // Hapticsの初期化 VibrationUtil.SetupHapticEngine(); } void Update() { // 画面タッチした瞬間を検出 if (Input.GetTouch(0).phase == TouchPhase.Began) { VibrationUtil.PlayHapticEngine(1f, 1f, 0.1f); } } }このコンポーネントをシーン上の適当なゲームオブジェクトにアタッチすれば、画面をタッチするたびにバイブレーションが発生します。簡単!
補足
その他、いろいろ実装に当たって必要だったことや気づいたことです。
■Haptics 対応端末かどうかの判定
APIにはそれらしいメソッドが見つからなかったので 「iPhone8以降、iOS13以上」 というCore Haptics の動作条件に従って、泥臭い方法で判定メソッド作ってみました。
VibrationUtil.csusing UnityEngine; ~~~中略~~~ public static bool IsSupportedHapticEngine() { #if UNITY_ANDROID || UNITY_EDITOR return false; #endif string versionString = SystemInfo.operatingSystem; versionString = versionString.Replace("iOS ", ""); float version = -1f; float.TryParse(versionString.Substring(0, 2), out version); if (version < 13) { // iOS13未満は未対応 return false; } string deviceString = SystemInfo.deviceModel; if (!deviceString.Contains("iPhone")) { // iPhone以外の端末(iPadなど)は未対応 return false; } deviceString = deviceString.Replace("iPhone", ""); float deviceID = -1f; float.TryParse(deviceString.Substring(0, 2), out deviceID); // デバイスモデル名が「iPhone10.0」以上(=iPhone8以降の機種) // 参照:http://www.enterpriseios.com/wiki/iOS_Devices IsSupportedHaptics = deviceID >= 10; return IsSupportedHaptics; }この方法だと、新型のiPhoneやiPadが出るたびにHaptics対応してるか確認しないといけないので困った…。公式のAPIで判定できるのを待つしかないのかなぁ。
▼絶対にマネしてはいけない方法
VibrationObjc.mm#import <UIKit/UIKit.h>
VibrationObjc.mmbool isSupportedHaptics() { NSNumber *supportLevel = [[UIDevice currentDevice] valueForKey:@"_feedbackSupportLevel"]; return [supportLevel intValue] == 2; }stack overflowのトピック で見かけた方法なのですが、プライベート変数でそれらしい数値を取ることができるらしく、「
2
以外のときは Haptic 未対応端末」と判定できるみたいです。
が! 非公開APIを触るのはバイナリを解析すれば分かるので、余裕でAppStoreからリジェクトくらいます。 やめようね!余談ですが、この指摘をくださった方曰く…
以前、実装の名前がたまたま非公開APIと完全一致しただけでもリジェクトされました T-T
恐ろしスギィ!
■アプリをバッググラウンドに入れたり端末をスリープさせると振動しなくなる
VibrationSwift.swift
で定義してるhapticEngine
が破棄されてる?VibrationTest.csprivate void OnApplicationPause(bool pauseStatus) { if (!pauseStatus) { // アプリ復帰時、HapticEngine初期化 VibrationUtil.SetupHapticEngine(); } }ひとまず上記のように、アプリが復帰するたびに初期化処理を呼べば再現しなくなりましたが、リークしてそうな気がして怖い。どうなんでしょう。
■Haptics でUXデザインするときのコツ(公式ガイドライン)
Human Interface Guidelines - Haptics の、 Designing with Haptics の部分に「こういうことを意識してね」「こういうことに気を付けてね」という指標が書いてあったので、ざっくり要約してみました。
- 「何で今振動したの?」という体験がないようにすること
- 単品ではなく、目に映る演出や音とセットで振動させること
- むやみやたらに振動させてユーザーにストレスを与えないようにすること
- 振動が苦手な人も「このくらいならOFFにしなくてもいいか」くらいがGood
- 同じシチュエーションでは同じ振動をさせること(一貫性)
- なんでここでは振動しないの?はNG
- できるだけいろんな実機×人間で試すこと(ストレスに感じるかどうか個体差が凄い)
- オプションでOFFにできるようにすること
- 操作量やレスポンスの把握しやすさを向上させる使い方を意識すること
- 最大スワイプに達したとき、UIをタッチできたときなど、判断スピード向上できるのが望ましい
- カメラやマイク、ジャイロの妨げにならないように気を付けること
たぶんこういう旨のことが書いてました。いずれも納得の内容。
iPhoneのシステム標準で使われてる Haptics Feedback は凄くナチュラルで小気味良いので、ここに辿り着くまでにAppleでたくさんの試行錯誤があったんだろうなぁ…。その結果が知見となってこのガイドラインを生んだと思うので、まずは従ってみたほうが良いでしょう。
タッチディスプレイとの組み合わせでデザインするUX なので、コンシューマーゲームのバイブレーションとはまた違う考え方をしないといけないですね。■OnPostprocessBuildで記述してるXcodeの設定箇所
CoreHaptics.framework
がなぜか一度Xcodeで直接追加しないとスクリプトによる追加が働かないっぽい挙動にハマったことがあったので、一応直接設定する場所も記載しておきます。(たぶんスクリプトで大丈夫ですが…念のため…)▼CoreHaptics.framework
▼「SWIFT_OBJC_INTERFACE_HEADER_NAME」、「SWIFT_VERSION」
Swiftのバージョンは、ここで選択できる数字をスクリプトで指定すればよいかと
参考記事
▼Core Haptics
- Core Haptics 公式ドキュメント
- 忙しい人のためのCore Haptics
- iOS13で公開予定の「Core Haptics」を使って、Haptic Feedback (触覚フィードバック) するコードを書いてみた
- Check if device supports UIFeedbackGenerator in iOS 10
- Haptics 対応端末かどうかの判定に関する stack overflow のトピック
▼Unity - iOSプラグイン - Swift 連携
▼その他
- 投稿日:2020-10-14T22:55:29+09:00
swift実践入門output Chapter4 コレクションを表す型
CollectionとしてのString型
String型は単一の文字を表すCharacter型のコレクションとして定義されており、文字の列挙や文字数のカウントなどの機能を持つ
chapter4.swift//StringIndex型 文字列内の位置を表すかた let string = "abcdefghijelmnopqustuvwxyza" let startString = string[string.startIndex] print(startString) //a //n番目、最後のindexを取得するには let string2 = "abcdefghijelmnopqustuvwxyza" let startString2 = string2.startIndex let bIndex = string.index(startString2, offsetBy: 8) print(bIndex) let bb = string[bIndex] print(bb) //i let endString = string2.endIndex let cIndex = string.index(endString, offsetBy: -1) let cc = string[cIndex] print(cc) //countプロパティを用いて要素数を取得 print(string2.count) //27シーケンス
その要素に一方向から順次アクセス可能なデータ構造
コレクションはシーケンスを包括する概念
一方向からの順次アクセスと特定のインデックスの値へ直接アクセスが可能なデータ構造Sequenceプロトコル 要素へ順次アクセス
chapter4.swift//forEach(_:)メソッド 要素に対して順次アクセス let darray = [1,2,3,4,5,6,7] var enumerated = [] as [Int] darray.forEach ({ element in enumerated.append(element) }) print(enumerated) //[1, 2, 3, 4, 5, 6, 7] //filterd(_:)メソッド 要素を絞り込む let filtered = darray.filter ({ element in element % 2 == 0 }) print(filtered) //[2, 4, 6] //map(_:)メソッド 要素を変換する //全ての要素を特定の要素を用いて変換 let double = darray.map({element in element * 2}) print(double) //[2, 4, 6, 8, 10, 12, 14] //別の型のシーケンスへと変換 Int → String let farray = [1,2,3,4,5] let converted = farray.map({element in String(element)}) print(converted) //["1", "2", "3", "4", "5"] //flatMap(_:)メソッド 要素をシーケンスに変換し、それを一つのシーケンスに追加 let sss = [1,2,3] let ttt = sss.flatMap({value in [value, value * 2]}) print(ttt) //compacttMap(_:)メソッド 要素を、失敗する可能sるのある処理を用いて変換する //全ての要素を特定の処理で変換するが、変換できない値は無視する let ggg = ["abc","123","kjg","456"] let integers = ggg.compactMap({value in Int(value)}) print(integers) //[123, 456] //reduce(_:)メソッド 要素を1つの値にまとめる let sarray = [1,2,3,4,5,6,7,8,9] let sum = sarray.reduce(0, {result, element in result + element} ) print(sum) //45 let concat = sarray.reduce("", {result, element in result + String(element)}) print(concat) //123456789 //Collectionプロトコル サブスクリプトによる要素へのアクセス let warray = [1,2,2,3,2,1,2,3,4] print(warray.count) //9 print(warray.isEmpty) //false print(warray.first) //Optional(1) print(warray.last) //Optional(4) print(warray[3]) //3
- 投稿日:2020-10-14T22:55:29+09:00
swift実践入門output Chapter4 コレクションを表す型 後編
CollectionとしてのString型
String型は単一の文字を表すCharacter型のコレクションとして定義されており、文字の列挙や文字数のカウントなどの機能を持つ
chapter4.swift//StringIndex型 文字列内の位置を表すかた let string = "abcdefghijelmnopqustuvwxyza" let startString = string[string.startIndex] print(startString) //a //n番目、最後のindexを取得するには let string2 = "abcdefghijelmnopqustuvwxyza" let startString2 = string2.startIndex let bIndex = string.index(startString2, offsetBy: 8) print(bIndex) let bb = string[bIndex] print(bb) //i let endString = string2.endIndex let cIndex = string.index(endString, offsetBy: -1) let cc = string[cIndex] print(cc) //countプロパティを用いて要素数を取得 print(string2.count) //27シーケンス
その要素に一方向から順次アクセス可能なデータ構造
コレクションはシーケンスを包括する概念
一方向からの順次アクセスと特定のインデックスの値へ直接アクセスが可能なデータ構造Sequenceプロトコル 要素へ順次アクセス
chapter4.swift//forEach(_:)メソッド 要素に対して順次アクセス let darray = [1,2,3,4,5,6,7] var enumerated = [] as [Int] darray.forEach ({ element in enumerated.append(element) }) print(enumerated) //[1, 2, 3, 4, 5, 6, 7] //filterd(_:)メソッド 要素を絞り込む let filtered = darray.filter ({ element in element % 2 == 0 }) print(filtered) //[2, 4, 6] //map(_:)メソッド 要素を変換する //全ての要素を特定の要素を用いて変換 let double = darray.map({element in element * 2}) print(double) //[2, 4, 6, 8, 10, 12, 14] //別の型のシーケンスへと変換 Int → String let farray = [1,2,3,4,5] let converted = farray.map({element in String(element)}) print(converted) //["1", "2", "3", "4", "5"] //flatMap(_:)メソッド 要素をシーケンスに変換し、それを一つのシーケンスに追加 let sss = [1,2,3] let ttt = sss.flatMap({value in [value, value * 2]}) print(ttt) //compacttMap(_:)メソッド 要素を、失敗する可能sるのある処理を用いて変換する //全ての要素を特定の処理で変換するが、変換できない値は無視する let ggg = ["abc","123","kjg","456"] let integers = ggg.compactMap({value in Int(value)}) print(integers) //[123, 456] //reduce(_:)メソッド 要素を1つの値にまとめる let sarray = [1,2,3,4,5,6,7,8,9] let sum = sarray.reduce(0, {result, element in result + element} ) print(sum) //45 let concat = sarray.reduce("", {result, element in result + String(element)}) print(concat) //123456789 //Collectionプロトコル サブスクリプトによる要素へのアクセス let warray = [1,2,2,3,2,1,2,3,4] print(warray.count) //9 print(warray.isEmpty) //false print(warray.first) //Optional(1) print(warray.last) //Optional(4) print(warray[3]) //3
- 投稿日:2020-10-14T22:24:40+09:00
swift実践入門output Chapter4 コレクションを表す型
コレクションとは値の集まりのこと
配列を表す
Array< Element>型辞書を表す
Dictionary< Key, Value>型範囲を表す
Range< Bound>型配列を表す Array< Element >型
実際にはArray< Int >型、Array< String >型のように使う
chapter4.swiftlet a = [1,2,3] let b = ["a","b","c"] let array :[Int] = [] var strings = ["aaa","bbb","ccc"] var strings1 = strings[0] print(strings1) //aaa strings[2] = "gagaga" print(strings) ["aaa", "bbb", "gagaga"] //末尾に追加 strings.append("yamato") print(strings) //["aaa", "bbb", "gagaga", "yamato"] //任意の位置に追加 strings.insert("mama", at: 2) print(strings) //["aaa", "bbb", "mama", "gagaga", "yamato"] //削除は3タイプ var integer = [1,2,3,4,5] integer.remove(at: 2) integer integer.removeLast() integer integer.removeAll() integer辞書を表す Dictionary< Key, Value >型
chapter4.swiftlet dictionary = ["Key":1] let value = dictionary["key"] print(value) //["key": 1] //変更 var dictionary1 = ["key": 2] dictionary1["key"] = 1 print(dictionary1) //追加 var dictionary2 = ["key":3] dictionary2["key2"] = 4 print(dictionary2) //["key2": 4, "key": 3] //削除 var dictionary3 = ["key":5] dictionary3["key"] = nil print(dictionary3) //[:]範囲型 範囲を表す型
chapter4.swift//末尾を含まない範囲 countableRange<Bound>型 let range = 1..<4 for value in range { print(value) } //1 //2 //3 //... 末尾を含む型 CountableClosedRange<Bound>型 let range2 = 1...4 for value in range2 { print(value) } //1 //2 //3 //4 let range3 = 1...5 print(range3.upperBound) print(range3.lowerBound) //5 //1 //値が範囲に含まれるかの判定 let range4 = 1..<10 print(range4.contains(4)) print(range4.contains(10)) //true //false
- 投稿日:2020-10-14T22:24:40+09:00
swift実践入門output Chapter4 コレクションを表す型 前編
コレクションとは値の集まりのこと
配列を表す
Array< Element>型辞書を表す
Dictionary< Key, Value>型範囲を表す
Range< Bound>型配列を表す Array< Element >型
実際にはArray< Int >型、Array< String >型のように使う
chapter4.swiftlet a = [1,2,3] let b = ["a","b","c"] let array :[Int] = [] var strings = ["aaa","bbb","ccc"] var strings1 = strings[0] print(strings1) //aaa strings[2] = "gagaga" print(strings) ["aaa", "bbb", "gagaga"] //末尾に追加 strings.append("yamato") print(strings) //["aaa", "bbb", "gagaga", "yamato"] //任意の位置に追加 strings.insert("mama", at: 2) print(strings) //["aaa", "bbb", "mama", "gagaga", "yamato"] //削除は3タイプ var integer = [1,2,3,4,5] integer.remove(at: 2) integer integer.removeLast() integer integer.removeAll() integer辞書を表す Dictionary< Key, Value >型
chapter4.swiftlet dictionary = ["Key":1] let value = dictionary["key"] print(value) //["key": 1] //変更 var dictionary1 = ["key": 2] dictionary1["key"] = 1 print(dictionary1) //追加 var dictionary2 = ["key":3] dictionary2["key2"] = 4 print(dictionary2) //["key2": 4, "key": 3] //削除 var dictionary3 = ["key":5] dictionary3["key"] = nil print(dictionary3) //[:]範囲型 範囲を表す型
chapter4.swift//末尾を含まない範囲 countableRange<Bound>型 let range = 1..<4 for value in range { print(value) } //1 //2 //3 //... 末尾を含む型 CountableClosedRange<Bound>型 let range2 = 1...4 for value in range2 { print(value) } //1 //2 //3 //4 let range3 = 1...5 print(range3.upperBound) print(range3.lowerBound) //5 //1 //値が範囲に含まれるかの判定 let range4 = 1..<10 print(range4.contains(4)) print(range4.contains(10)) //true //false
- 投稿日:2020-10-14T21:34:26+09:00
SwiftでProperty List(.plist)への書き込みと読み込みを実装する
Property List(.plistファイル)へのデータ書き込みとデータ読み込みを、
PropertyListEncoder(Decoder)
とCodable
に準拠したモデルを利用して行います。前提
Swift: 5.0
実装
モデルの用意
Property Listに保存したいデータのモデルを、
Codable
に準拠する形で作成します。
今回は例として、タイトルと著者名を要素に持つBook
をモデルとします。Book.swiftstruct Book: Codable { var title: String var writerName: String }データの書き込み
ファイル操作は
FileManager
を用いて行います。
先程用意したモデルを、PropertyListEncoder
を使ってエンコードすることで、Property Listへの書き込みが行えます。BookManager.swiftclass BookManager { // 扱うProperty ListのURL Path static private var plistURL: URL { let documents = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! return documents.appendingPathComponent("book.plist") } static func write(book: Book) { let encoder = PropertyListEncoder() // 保存したいデータをエンコード guard let data = try? encoder.encode(book) else { return } // すでにProperty Listが存在する場合は上書き。そうでない場合は新しく作成 if FileManager.default.fileExists(atPath: plistURL.path) { try? data.write(to: plistURL) } else { FileManager.default.createFile(atPath: plistURL.path, contents: data, attributes: nil) } } }データの読み込み
データの書き込みと同様に、ファイル操作は
FileManager
を用いて行います。
PropertyListDecoder
を用いてProperty Listから読み込んだデータをBook
モデルにデコードしています。BookManager.swiftclass BookManager { // 省略 // static func load() -> Book { let decoder = PropertyListDecoder() // 保存先のProperty Listからデータを読み込んでデコード guard let data = try? Data.init(contentsOf: plistURL), let book = try? decoder.decode(Book.self, from: data) else { return Book(title: "", writerName: "") } return book } }終わりに
Codable
及びPropertyListEncoder(Decoder)
を使うことで、Property List(.plist)でのデータ操作を楽に実装することができました。参考
- 投稿日:2020-10-14T17:34:40+09:00
【ReactNative】タイムゾーン無し文字列をDate()に渡した時の挙動がiOS14から異なる件
概要
iOS14
の公開に伴いreact-native
で開発したアプリの挙動を確認していたら、何やら日時がずれているのを発見しました。
細かく処理を追跡していったところ、どうやらDate(string)
の挙動が若干変わったようです。※個人ブログで同様の記事を書いています。今後追記があればそちらを更新していきます。
環境
react-native@0.63.1
iOS14
2020/10/14
時点の情報です(今後のアップデートで修正される可能性あり)コンストラクで文字列を渡している場合は注意
例えば以下のように文字列から
Date
型のコンストラクタを利用した場合です。
サンプル①はISO-8601
の拡張形式のタイムゾーンを指定していない形式です(この時点でオラオラ仕様なのも問題なのですが・・・)。// サンプル① const sample1 = '2020-10-10T00:00:00'; // サンプル② const sample2 = '2020/10/10 00:00:00'; // それぞれDate型にキャストする const result1 = new Date(text1); const result2 = new Date(text2); // 結果を表示 console.log('result1 --> ', result1.toString()); console.log('result2 --> ', result2.toString());この実行結果が
iOS14
前後で異なります。
それぞれ以下の通りです。出力結果(iOS14未満)
result1 --> Wed Oct 14 2020 09:00:00 GMT+0900 (JST) result2 --> Wed Oct 14 2020 00:00:00 GMT+0900 (JST)出力結果(iOS14)
result1 --> Wed Oct 14 2020 00:00:00 GMT+0900 (JST) result2 --> Wed Oct 14 2020 00:00:00 GMT+0900 (JST)サンプル①の場合の結果が変わっているのが分かります。
まとめ
そもそも渡している文字列が正しいフォーマットから少し外れたものであるのも悪いですが、
iOS14
前後でタイムゾーン無しの文字列を渡した際の解析処理が変わったのだと思います。
react-native
固有の事象なのか、iOS
のネイティブ部分が変わった故なのかいまいち判断がつかない(そこまで知識がない)のですが、同様の事象に遭遇される方もいるかと思うので記事にしました。
- 投稿日:2020-10-14T13:20:14+09:00
ヒラギノフォント使用時のUILabelの高さ調整
概要
iOS14にて、ヒラギノフォントを指定していたUILabelで、 g や jといった、
下にはみ出す文字の下部がはみ出すようになったので、その対応。問題の詳しい解説などは以下などを参考にしてください
https://koze.hatenablog.jp/entry/2020/05/11/093000
http://akisute.com/2015/11/ios.html環境
Swift5
Xcode11.3 (Xcode12.0でも動作確認)解決作
*正直この対応で本当に問題がないのか、あまり自信はありません。間違いなどありましたら、ご指摘いただけますと幸いです??♀️
Extentionで関数使って修正している情報は見つかったのですが、
今回すでに問題が起こりうるヒラギノをつかった箇所がたくさんあり、ヒラギノフォントの設定はxibでやっていたため、
あまりコードはいじりたくないなあというところがあったので、カスタムクラスを作成して、そのカスタムクラスをxibで設定すれば、勝手に調整するようにしてみました。
調整の内容じたいは上でも紹介している以下と同じです。
http://akisute.com/2015/11/ios.htmlimport UIKit class HiraginoUILabel: UILabel { override var intrinsicContentSize: CGSize { var size = super.intrinsicContentSize // ヒラギノフォントで日本語と英語が混じっている場合のラベルサイズずれ対応 if let font = self.font, font.familyName.hasPrefix("Hiragino") { // 念の為ヒラギノフォントであること確認 size.width = ceil(size.width); size.height = ceil(size.height + abs(font.descender)); } return size; } }これで、xib側で、ヒラギノを指定しているLabelにカスタムクラスを指定すると、高さが自動で調節されます。
念の為カスタムクラス側でフォントを確認しているため、他のフォントに変わってクラス指定を外すことを忘れても、何もしないようにしています。
以上です。
- 投稿日:2020-10-14T13:20:14+09:00
[iOS14]ヒラギノフォント使用時のUILabelの高さ調整
概要
iOS14にて、ヒラギノフォントを指定していたUILabelで、 g や jといった、
下にはみ出す文字の下部が見切れるようになったので、その対応。問題の詳しい解説などは以下などを参考にしてください
https://koze.hatenablog.jp/entry/2020/05/11/093000
http://akisute.com/2015/11/ios.html環境
Swift5
Xcode11.3 (Xcode12.0でも動作確認)解決作
*正直この対応で本当に問題がないのか、あまり自信はありません。間違いなどありましたら、ご指摘いただけますと幸いです??♀️
Extentionで関数使って修正している情報は見つかったのですが、
今回すでに問題が起こりうるヒラギノをつかった箇所がたくさんあり、かつヒラギノの設定はxibでやっていたため、修正もxibできる方法を探しました。サイズを調整するカスタムクラスを作成しました。
調整の内容自体は上でも紹介している以下の内容と同じです。
http://akisute.com/2015/11/ios.htmlimport UIKit class HiraginoUILabel: UILabel { override var intrinsicContentSize: CGSize { var size = super.intrinsicContentSize // ヒラギノフォントで日本語と英語が混じっている場合のラベルサイズずれ対応 if let font = self.font, font.familyName.hasPrefix("Hiragino") { // 念の為ヒラギノフォントであること確認 size.width = ceil(size.width); size.height = ceil(size.height + abs(font.descender)); } return size; } }これで、xib側で、ヒラギノを指定しているLabelにカスタムクラスを指定すると、高さが自動で調節されます。
念の為カスタムクラス側でフォントを確認しているため、他のフォントに変わってクラス指定を外すことを忘れても、何もしないようにしています。
以上です。
- 投稿日:2020-10-14T10:32:44+09:00
[Swift5]"IBM Watson ToneAnalyzer"を使用して感情分析を行う
投稿のポイント
個人アプリ開発の機能として
IBM Watson ToneAnalyzer
を用いて感情分析機能
を実装したいので、私なりに公式ドキュメントを参考にしながら実装してみました。今回アウトプットするのは初期段階のAPIキーの取得
と、実際に分析を行い、分析結果を表示する
といったところです。APIキーの取得
まず、
Watson IBM Cloud
のアカウントを作成します。下記urlから公式ページに遷移し、右上にある登録
からアカウントを作成して下さい。
https://cloud.ibm.com/developer/watson/services
登録が完了したら下記画像のようにWatsonサービスのToneAnalyze
を選択してください。
続いて、リージョンを東京、プランのライトを選択して作成をクリックします。
すると専用ページに遷移するので資格情報の取得を選択し、APIキー
とURL
を取得します。この時点で
APIキー
とURL
をプロジェクトに記述し、コメントアウトしておくことをオススメします!ToneAnalyzerをインポート
APIキーの取得はできたので、ここからコードの実装に参りたいと思います。まず、Cocoapodsを使用して
ToneAnalyzer
をインポートします。まだ、podsをインストールしたことがないという方は下記urlを参考にしてください。
https://qiita.com/ShinokiRyosei/items/3090290cb72434852460それではpodのインストールを行います。
Podfile.target 'アプリ名' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! # Pods for アプリ名 pod 'IBMWatsonToneAnalyzerV3', '~> 3.6.0' #ここを記述 end次に、フレームワークを使用したいコントローラーにインポートします。
ここで注意していただきたいのは、IBMWatsonプレフィックスとバージョンサフィックスを除外します。
IBMWatsonToneAnalyzerV3
の、IBMWatson
と、V3
を削除してimport ToneAnalyzer
と記述するということです。APIの認証
フレームワークのインポートが完了したら次は認証フェーズです。スタンダートな方法として、
IBM Cloud Identity and Access Management(IAM)
を使用し、APIキーとAPIのversionとURLを定義します。ViewController.swiftimport UIKit import ToneAnalyzer class ViewController: UIViewController { //WatsonAPIキーを定義 let authenticator = WatsonIAMAuthenticator(apiKey: "ここに取得したAPIキーを添付") override func viewDidLoad() { super.viewDidLoad() //WatsonAPIのversionとURLを定義 let toneAnalyzer = ToneAnalyzer(version: "2017-09-21", authenticator: authenticator) toneAnalyzer.serviceURL = "ここに取得したURLを添付" } }ここで注意点ですが、
serviceURL
は APIを作成した時に設定した場所によって変化します。下記に列挙しているので、設定した場所に合わせてtoneAnalyzer.serviceURL
に代入してあげてください。場所別のサービスURL ダラス: https://api.us-south.tone-analyzer.watson.cloud.ibm.com ワシントンDC: https://api.us-east.tone-analyzer.watson.cloud.ibm.com フランクフルト: https://api.eu-de.tone-analyzer.watson.cloud.ibm.com ロンドン: https://api.eu-gb.tone-analyzer.watson.cloud.ibm.com ソウル: https://api.kr-seo.tone-analyzer.watson.cloud.ibm.com シドニー: https://api.au-syd.tone-analyzer.watson.cloud.ibm.com Tokyo: https://api.jp-tok.tone-analyzer.watson.cloud.ibm.comエラー処理とデータ処理
続いてエラー処理とデータ処理を記述します。と、その前に分析に使用するサンプルテキストを宣言します。
ViewController.swiftimport UIKit import ToneAnalyzer class ViewController: UIViewController { //WatsonAPIキーを定義 let authenticator = WatsonIAMAuthenticator(apiKey: "ここに取得したAPIキーを添付") //分析用サンプルテキスト let sampleText = """ Team, I know that times are tough! Product \ sales have been disappointing for the past three \ quarters. We have a competitive product, but we \ need to do a better job of selling it! """ override func viewDidLoad() { super.viewDidLoad() //WatsonAPIのversionとURLを定義 let toneAnalyzer = ToneAnalyzer(version: "2017-09-21", authenticator: authenticator) toneAnalyzer.serviceURL = "ここに取得したURLを添付" } }それでは定義した
sampleText
を使って分析を行います。switch文で条件分岐を行い、ステータスコードによって表示される内容を変更します。ステータスコードの200範囲は成功、400範囲は障害、500範囲は内部システムエラーを表しております。ViewController.swiftimport UIKit import ToneAnalyzer class ViewController: UIViewController { //WatsonAPIキーを定義 let authenticator = WatsonIAMAuthenticator(apiKey: "q6GL14WCXtIbNgwYazVmBDNGlyd3jmxglni-pmk96g0z") //分析用サンプルテキスト let sampleText = """ Team, I know that times are tough! Product \ sales have been disappointing for the past three \ quarters. We have a competitive product, but we \ need to do a better job of selling it! """ override func viewDidLoad() { super.viewDidLoad() //WatsonAPIのversionとURLを定義 let toneAnalyzer = ToneAnalyzer(version: "2017-09-21", authenticator: authenticator) toneAnalyzer.serviceURL = "https://api.jp-tok.tone-analyzer.watson.cloud.ibm.com" //エラー処理 toneAnalyzer.tone(toneContent: .text(sampleText)){ #ここでsampleTextを定義 response, error in if let error = error { switch error { case let .http(statusCode, message, metadata): switch statusCode { case .some(404): // Handle Not Found (404) exceptz1zion print("Not found") case .some(413): // Handle Request Too Large (413) exception print("Payload too large") default: if let statusCode = statusCode { print("Error - code: \(statusCode), \(message ?? "")") } } default: print(error.localizedDescription) } return } //データ処理 guard let result = response?.result else { print(error?.localizedDescription ?? "unknown error") return } print(result) //ステータスコードの表示(200範囲は成功、400範囲は障害、500範囲は内部システムエラー) print(response?.statusCode as Any) //ヘッダーパラメータ print(response?.headers as Any) } } }実行
ここまで記述することができたらビルドを行い、実際に分析情報を引っ張ってこれるかの確認を行います。
デバックエリアに下記のように分析結果が表示されれば成功です。ToneAnalysis(documentTone: ToneAnalyzer.DocumentAnalysis(tones: Optional([ToneAnalyzer.ToneScore(score: 0.6165, toneID: "sadness", toneName: "Sadness"), ToneAnalyzer.ToneScore(score: 0.829888, toneID: "analytical", toneName: "Analytical")]), toneCategories: nil, warning: nil), sentencesTone: Optional([ToneAnalyzer.SentenceAnalysis(sentenceID: 0, text: "Team, I know that times are tough!", tones: Optional([ToneAnalyzer.ToneScore(score: 0.801827, toneID: "analytical", toneName: "Analytical")]), toneCategories: nil, inputFrom: nil, inputTo: nil), ToneAnalyzer.SentenceAnalysis(sentenceID: 1, text: "Product sales have been disappointing for the past three quarters.", tones: Optional([ToneAnalyzer.ToneScore(score: 0.771241, toneID: "sadness", toneName: "Sadness"), ToneAnalyzer.ToneScore(score: 0.687768, toneID: "analytical", toneName: "Analytical")]), toneCategories: nil, inputFrom: nil, inputTo: nil), ToneAnalyzer.SentenceAnalysis(sentenceID: 2, text: "We have a competitive product, but we need to do a better job of selling it!", tones: Optional([ToneAnalyzer.ToneScore(score: 0.506763, toneID: "analytical", toneName: "Analytical")]), toneCategories: nil, inputFrom: nil, inputTo: nil)])) Optional(200) Optional(["x-service-build-number": "2020-10-12T05:02:12", "x-dp-watson-tran-id": "e7f11a38-2089-4860-8414-7f1e3878f449", "Content-Security-Policy": "default-src \'none\'", "x-xss-protection": "1; mode=block", "x-service-api-version": "null; 2017-09-21", "x-global-transaction-id": "e7f11a38-2089-4860-8414-7f1e3878f449", "Date": "Wed, 14 Oct 2020 01:20:56 GMT", "Server": "watson-gateway", "x-powered-by": "Servlet/3.1", "X-EdgeConnect-Origin-MEX-Latency": "212", "Content-Language": "en-US", "x-request-id": "e7f11a38-2089-4860-8414-7f1e3878f449", "Access-Control-Allow-Origin": "*", "Content-Length": "726", "Cache-Control": "no-store", "Content-Type": "application/json", "Pragma": "no-cache", "Connection": "keep-alive", "Strict-Transport-Security": "max-age=31536000; includeSubDomains;", "X-EdgeConnect-MidMile-RTT": "7", "x-content-type-options": "nosniff"])
ViewController.swift
で記述したprintの内容がしっかりと表示されていますね。
ステータスコードも(200)
と表示されているので成功ということです。ViewController.swiftprint(result) //ステータスコードの表示(200範囲は成功、400範囲は障害、500範囲は内部システムエラー) print(response?.statusCode as Any) //ヘッダーパラメータ print(response?.headers as Any)これで実際にアプリケーションに組み込む実装はまだですが、初期段階の
APIキーの取得
と、実際に分析を行い、分析結果を表示する
という点に関して実装できました!最後に
引き続き個人アプリの開発に取り組みながらアウトプットを行いますので、参考にしていただけると幸いです。最後までご覧いただきありがとうございました!