ttlog

日々の開発で得た知見の技術メモ。モバイルアプリネタが多いです。

Xcode10のちょっとした変更点: StoryboardからOutlet/Actionを作成する際のデフォルトConnection

Xcode10でStoryboard上のUIButtonからコード上にConnection(Outlet/Action)を作成する際、挿入位置によってデフォルトで選択済のConnectionが変わるようになっていました。(以前は"Outlet"固定だったかと思います)

具体的には、コードのViewControllerクラスに定義しているメソッドの下に挿入すると、デフォルトのConnectionが"Action"に変わるようです。

  • メソッドの上に挿入しようとする場合、Outletがデフォルトになる

f:id:tommy10344:20181029124357j:plain

  • メソッドの下に挿入しようとする場合、Actionがデフォルトになる

f:id:tommy10344:20181029124410j:plain

メソッド以外の場合は以前までと同様に"Outlet"がデフォルトになるようです。 以下の要素の下に挿入しようとする場合でも、"Outlet"がデフォルトになることを確認しました。

  • Stored Properties
  • Computed Properties
  • Nested Types

ちょっとしたことではありますが、Actionを作ろうとして間違ってOutletを作ってしまったり、いちいちActionに切り替える等の面倒だったことが改善される良い変更だと思います。

Androidエミュレータで動作させるSystem Imageの違い

Android開発時にインストールするエミュレータのSystem Imageの違い、特に「Google APIs」と「Google Play」の違いがよく分かっていなかったのでメモしておきます。

f:id:tommy10344:20181025155133p:plain

Google Play System Image

  • Google Play Storeがインストールされており、実機と同じようにアプリがStoreからインストール出来る
  • root化されていない
  • イメージを動作させる仮想デバイスに制限がある。電話端末だと以下が対応しているようです。(Android Studio 3.2.1時点)

実機に近い環境だと思うので、基本的にはこちらを選択しておけば良さそうですね。

Google APIs System Image

CPU命令セットの違い

参考

Windowsで高速AVDを利用するまでの手順をものすごく詳しく解説

android - Difference between Google API intel x86 atom and Google Play intel x86 atom sytem image - Stack Overflow

adb shellでアプリのプライベートデータを確認する方法

備忘録です。

アプリのプライベートデータは(確認した環境では)/data/data/[アプリのパッケージ名]内にあるようですが、通常のadb shellだけだとパーミッションエラーで中身を確認することが出来ませんでした。

手順

  • adb shell でシェルを起動し、
  • run-as [アプリのパッケージ名]パーミッションを取得する
$ adb shell
shell@xxx:/ $ run-as com.example.foo

(パッケージ名がcom.example.fooの場合)

以上でアプリのプライベートデータを確認することが出来ます。

Android(Xamarin) 文字列の横幅取得方法

Androidにおける文字列の横幅(dp)を取得する方法のメモです。 親Layoutの横幅と比較して、1行に何文字表示出来るか等の計算が出来たりします。

現在Xamarinで開発中のため、コードもXamarin(C#)になってしまっていますが、Android Nativeでも書き方はそう変わらないかと思います。

文字列のサイズ取得

TextViewクラスから取得出来るPaintオブジェクトを使い、MeasureText()を呼び出します。

measureText)

using Android.App;
using Android.Content.Res;
using Android.Util;
using Android.Widget;

namespace XXX
{
    public class StringWidthCalculator
    {
        public double CalculateStringWidth(string str, double fontSize)
        {
            var textView = new TextView(Application.Context);
            textView.SetTextSize(ComplexUnitType.Sp, (float)fontSize);
            var widthOfPixel = textView.Paint.MeasureText(str);
            return widthOfPixel / Resources.System.DisplayMetrics.Density;
        }
    }
}

TextViewにフォントサイズを設定しておくことでフォントサイズに応じた文字列のサイズが取得出来るようです。 ComplexUnitType.Spとしているため、ユーザのフォントサイズ設定に応じて横幅が変動します。

また、MeasureText()で返却される横幅はpixel単位のため、この関数から返却する際にdpに変換しています。

使い方

var str = "abcde";
double fontSize = 13;
var calculator = new StringWidthCalculator();
var width = calculator.CalculateStringWidth(str, fontSize);

サンプルコード

github.com

iOS 文字列のサイズ取得方法

iOSにおける文字列のサイズ取得方法のメモです。 Viewの横幅と比較して、1行に何文字表示出来るか等の計算が出来たりします。

文字列のサイズ取得

NSStringクラスにサイズが取得出来るメソッドがあるので、こちらを使います。

size(withAttributes:)

UIFontを渡してあれば、フォントサイズに応じた文字列のサイズが取得出来ます。

// 今回はStringクラスのExtensionとして実装

import UIKit

public extension String {
    public func size(with font: UIFont) -> CGSize {
        let attributes = [NSAttributedStringKey.font : font]
        return (self as NSString).size(withAttributes: attributes)
    }
}

使い方

let font = UIFont.systemFont(ofSize: 13)
let size = "abcde".size(with: font)
let width = size.width
let height = size.height

戻り値はCGSizeになっていますが、こちらは1行に全文字を並べた場合のサイズを返すようです。つまり、文字数に応じてwidthだけが伸びていき、heightは常に1文字分の高さを返します。

  • フォントサイズ13

f:id:kurozu10344:20180815135537p:plain

  • フォントサイズ20

f:id:kurozu10344:20180815135541p:plain

  • フォントサイズ20 + 長い文字列

f:id:kurozu10344:20180815140157p:plain

iOS 連絡先一覧の取得

Contacts Frameworkを使用し、標準の連絡先アプリに登録されている連絡先一覧を取得する方法についてまとめてみます。

Contacts Framework公式ページ

サンプルコードは以下に置いています。

https://github.com/kurozu10344/ContactsSample

検証環境

  • iOS 11 シミュレータ
  • Xcode 9.4.1
  • Swift 4.1

連絡先一覧の取得

利用目的の記述(Info.plist)

まず、このFrameworkはユーザのプライバシーなデータにアクセスするため、ユーザに対して連絡先へアクセスする目的を提示する必要があります。この設定をターゲットのInfo.plistに追記します。

f:id:kurozu10344:20180728234759p:plain

KeyはPrivacy - Contacts Usage Description(Raw Keyの場合はNSContactsUsageDescription)を、TypeはStringを選択し、Valueにユーザに提示する利用目的を記述します。

NSContactsUsageDescription

ここで設定した文言が、後ほど説明するアクセス許可要求時の確認アラートに表示されます。

この設定を行わずにContacts FrameworkのAPIを呼び出そうとした場合、以下のようなエラーメッセージを表示してアプリが落ちます。よって設定は必須です。

This app has crashed because it attempted to access privacy-sensitive data without a usage description.  The app's Info.plist must contain an NSContactsUsageDescription key with a string value explaining to the user how the app uses this data.

アクセス許可の確認・要求

ここからコーディングに入ります。

まず、実際に連絡先を取得する前に、ユーザに連絡先アクセスの許可を得ているかを確認し、まだの場合はアクセス許可を求める必要があります。

        let status = CNContactStore.authorizationStatus(for: .contacts)
        switch status {
        case .notDetermined:
            // 初回アクセス時
            // アクセス許可要求
            CNContactStore().requestAccess(for: .contacts) { (granted, error) in
                if granted {
                    // アクセス許可された
                }
                else {
                    // アクセス拒否された
                }
            }

        case .authorized:
            // アクセス許可済

        case .restricted:
            // ペアレンタルコントロール等の機能制限により利用不可

        case .denied:
            // アクセス拒否済
        }
  1. CNContactStore.authorizationStatus(for:)を呼び出し、許可状態を確認する。
  2. 許可状態がCNAuthorizationStatus.notDeterminedの場合、CNContactStore.requestAccess(for:completionHandler:)を呼び出し、アクセス許可を要求する。
  3. ↑で渡したcompletionHandlerの第一引数(ソース中ではgranted)がtrueの場合、連絡先へのアクセスが可能になる。

CNContactStore.requestAccess(for:completionHandler:)を呼び出した際に以下のようなアラートが表示され、Info.plistに記述した利用目的が表示されます。

f:id:kurozu10344:20180728234802p:plain

取得処理

アクセスが許可された、あるいは許可済の場合、ようやく連絡先の取得を開始します。

    private func loadContacts() {
        DispatchQueue.global().async {
            let store = CNContactStore()
            let keys: [CNKeyDescriptor] = [
                CNContactFamilyNameKey as CNKeyDescriptor,
                CNContactGivenNameKey as CNKeyDescriptor,
                CNContactEmailAddressesKey as CNKeyDescriptor,
                CNContactPhoneNumbersKey as CNKeyDescriptor,
                CNContactFormatter.descriptorForRequiredKeys(for: .fullName)]
            let fetchRequest = CNContactFetchRequest(keysToFetch: keys)
            var contacts: [CNContact] = []
            try? store.enumerateContacts(with: fetchRequest) { contact, cursor in
                contacts.append(contact)
            }
            self.contacts = contacts
            DispatchQueue.main.async {
                // Viewに表示、等
            }
        }
    }
  • self.contactsは[CNContact]型のプロパティです。
  • 連絡先取得にはI/O操作が含まれますが、同期的に実行されるため、バックグラウンドでの処理が推奨されています。

    Because the contact store methods are synchronous, it is recommended that you use them on background threads.

  • keysには連絡先内の必要な項目を列挙します。ここに指定していない項目を取得しようとした場合(例えばCNContactEmailAddressKeyを指定せずにcontact.emailAddressesを呼び出した場合)、アプリがクラッシュします。

サンプルソースではCNContactStore.enumerateContacts(with:usingBlock:)を呼び出し、クロージャの引数に渡された連絡先(contact: CNContact)をリストに詰め込んでいます。

あとはこの連絡先から各種情報を取得して処理していけば良いかと思います。

フルネームの取得

フルネームの取得はCNContactを直接使わず、CNContactFormatter.string(from:style:)を使用します。(まぁ日本人ならfamilyNamegivenNameを組み合わせるだけで良いのかもしれませんが。。。)

let fullName = CNContactFormatter.string(from: contact, style: .fullName)

style: .fullNameの箇所に取得したいデータ形式(CNContactFormatterStyle)を指定します。2018年7月現在はフルネームとフルネームのふりがなが指定可能です。

またこの場合、fetchRequestに指定するkeyに以下を含める必要があります。

CNContactFormatter.descriptorForRequiredKeys(for: .fullName)

自分のカードは判別不可

iOSでは、特定の連絡先を自分の情報として登録が出来、連絡先アプリに自分のカード(My Card)として表示することが出来ますが、サードパーティアプリからはどの連絡先が自分のカードであるかは判別出来ないようです。 CNContactStore.unifiedMeContactWithKeys(toFetch:)というメソッドがありますが、こちらはmacOS専用のAPIのため、iOSからは使用出来ません。

https://forums.developer.apple.com/thread/67586

ファイル内の文字列を置換して上書き保存

便利でたまに使いたくなるのですが、すぐ忘れてしまうので備忘録です。 Perlワンライナーを使用します。

環境

OS version

macOS 10.13.5 (High Sierra)

Perl version

$ perl -v

This is perl 5, version 26, subversion 2 (v5.26.2) built for darwin-thread-multi-2level
...

単一ファイルの置換

$ perl -p -i -e 's/XXX/YYY/g' FilePath

FilePathが示すファイル中の'XXX'を'YYY'に置換する。

各オプションについては以下:

-p

入力ファイルの各行を、後述する-eオプションのスクリプトで処理して出力する。このオプションを追加することでsedコマンドのように扱える。

-i

入力ファイルに処理結果を上書きする。間違えた処理を上書きしてしまうと戻せないため、元ファイルはバックアップ推奨。

-e

実行するスクリプトを指定する。

複数ファイルの一括置換

単一ファイルの置換をfindコマンドで複数ファイルに適用するだけ。

$ find . -type f -exec perl -p -i -e 's/XXX/YYY/g' {} \;

参考

https://www.perl.com/pub/2004/08/09/commandline.html/