20201014のiOSに関する記事は10件です。

[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 での実装方法 を紹介します。

image.png

 上図のように、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.swift
import 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)

というようにパラメータを定義し、CHHapticEventparameters に渡すことで設定できます。
上記コードを例にすると、

let event = CHHapticEvent(eventType: .hapticContinuous, parameters: [intensity, sharpness, sustained, attackTime], relativeTime: 0, duration: TimeInterval(durationValue))

こんな感じです。(sustained の後に attackTime を追加)

■参考 - 遅延実行

CHHapticEventrelativeTime の引数を使うことで、遅延実行をすることもできます。
例えば「ブブッ」というような複数回の振動パターンを作るときに必要になるでしょう。

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.cs
using 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.cs
using 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.cs
using 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.mm
bool isSupportedHaptics() {
    NSNumber *supportLevel = [[UIDevice currentDevice] valueForKey:@"_feedbackSupportLevel"];
    return [supportLevel intValue] == 2;
}

stack overflowのトピック で見かけた方法なのですが、プライベート変数でそれらしい数値を取ることができるらしく、「2 以外のときは Haptic 未対応端末」と判定できるみたいです。
が! 非公開APIを触るのはバイナリを解析すれば分かるので、余裕でAppStoreからリジェクトくらいます。 やめようね!

余談ですが、この指摘をくださった方曰く…

以前、実装の名前がたまたま非公開APIと完全一致しただけでもリジェクトされました T-T

恐ろしスギィ!

■アプリをバッググラウンドに入れたり端末をスリープさせると振動しなくなる

VibrationSwift.swift で定義してる hapticEngine が破棄されてる?

VibrationTest.cs
private 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

image.png
 

▼「SWIFT_OBJC_INTERFACE_HEADER_NAME」、「SWIFT_VERSION」

image.png

Swiftのバージョンは、ここで選択できる数字をスクリプトで指定すればよいかと

参考記事

▼Core Haptics

▼Unity - iOSプラグイン - Swift 連携

▼その他

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

swift実践入門output Chapter4 コレクションを表す型

コレクションとは値の集まりのこと

配列を表す
Array< Element>型

辞書を表す
Dictionary< Key, Value>型

範囲を表す
Range< Bound>型

配列を表す Array< Element >型

実際にはArray< Int >型、Array< String >型のように使う

chapter4.swift
let 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.swift
let 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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

swift実践入門output Chapter4 コレクションを表す型 前編

コレクションとは値の集まりのこと

配列を表す
Array< Element>型

辞書を表す
Dictionary< Key, Value>型

範囲を表す
Range< Bound>型

配列を表す Array< Element >型

実際にはArray< Int >型、Array< String >型のように使う

chapter4.swift
let 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.swift
let 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
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SwiftでProperty List(.plist)への書き込みと読み込みを実装する

Property List(.plistファイル)へのデータ書き込みとデータ読み込みを、PropertyListEncoder(Decoder)Codableに準拠したモデルを利用して行います。

前提

Swift: 5.0

実装

モデルの用意

Property Listに保存したいデータのモデルを、Codableに準拠する形で作成します。
今回は例として、タイトルと著者名を要素に持つBookをモデルとします。

Book.swift
struct Book: Codable {
    var title: String
    var writerName: String
}

データの書き込み

ファイル操作はFileManagerを用いて行います。
先程用意したモデルを、PropertyListEncoderを使ってエンコードすることで、Property Listへの書き込みが行えます。

BookManager.swift
class 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.swift
class 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)でのデータ操作を楽に実装することができました。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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のネイティブ部分が変わった故なのかいまいち判断がつかない(そこまで知識がない)のですが、同様の事象に遭遇される方もいるかと思うので記事にしました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ヒラギノフォント使用時の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.html

import 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-14 13.16.08.png

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS14]ヒラギノフォント使用時のUILabelの高さ調整

概要

iOS14にて、ヒラギノフォントを指定していたUILabelで、 g や jといった、
下にはみ出す文字の下部が見切れるようになったので、その対応。

↓下部が見切れている。
スクリーンショット 2020-10-16 15.40.10.png

問題の詳しい解説などは以下などを参考にしてください
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.html

import 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-14 13.16.08.png

↓対応後
スクリーンショット 2020-10-16 15.40.15.png

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Swift5]"IBM Watson ToneAnalyzer"を使用して感情分析を行う

投稿のポイント

個人アプリ開発の機能としてIBM Watson ToneAnalyzerを用いて感情分析機能を実装したいので、私なりに公式ドキュメントを参考にしながら実装してみました。今回アウトプットするのは初期段階のAPIキーの取得と、実際に分析を行い、分析結果を表示するといったところです。

APIキーの取得

まず、Watson IBM Cloudのアカウントを作成します。下記urlから公式ページに遷移し、右上にある登録からアカウントを作成して下さい。
https://cloud.ibm.com/developer/watson/services
登録が完了したら下記画像のようにWatsonサービスのToneAnalyzeを選択してください。
image.png
続いて、リージョンを東京、プランのライトを選択して作成をクリックします。
image.png
すると専用ページに遷移するので資格情報の取得を選択し、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.swift
import 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.swift
import 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.swift
import 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.swift
print(result)
//ステータスコードの表示(200範囲は成功、400範囲は障害、500範囲は内部システムエラー)
print(response?.statusCode as Any)
//ヘッダーパラメータ
print(response?.headers as Any)

これで実際にアプリケーションに組み込む実装はまだですが、初期段階のAPIキーの取得と、実際に分析を行い、分析結果を表示するという点に関して実装できました!

最後に

引き続き個人アプリの開発に取り組みながらアウトプットを行いますので、参考にしていただけると幸いです。最後までご覧いただきありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む