しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

AndroidのXMLのIDの+について調べる

Androidアプリ開発で出てくるIDの+の意味について調べてみました。

<EditText android:id="@+id/editText" />

調べた所+を付けるのはIDを自動定義するためのようです。
+なしだと参照するだけなのですが、+ありだと新規定義&参照してくれます。

参考にしたのは下記事です。

android - @+id/とはなんですか? - スタック・オーバーフロー

実際にプラスを取るとエラーになります。

<EditText android:id="@id/editText" />

エラー内容は下のように「IDが見つからない」というものです。

error: resource id/editText (aka com.example.myapp:id/editText) not found.

IDが見つからないと言われたので試しに自分で定義してみました。
値は適当な数字を入れています。

<resources>
    <id name="editText">347284929378</id>
</resources>

実行すると下のようなエラーになりました。
どうやらIDは空かリソースの参照にする必要があるようです。

<id> inner element must either be a resource reference or empty.

空にしたら無事に動かすことができました。
先程のレイアウト側で置きていた「IDがない」というエラーもおきなくなりました。

<resources>
    <id name="editText" />
</resources>

上記のようにidを定義した状態で+idを使ってもエラーにはならないようです。

<EditText android:id="@+id/editText" />

それと複数箇所でIDの定義をすると警告が出ます。

<EditText android:id="@+id/editText" />
<EditText android:id="@+id/editText" />

警告内容は下のとおりです。

Duplicate id @+id/cbDrink, already defined earlier in this layout

こちらですが片方の+を取れば警告が出なくなりました。

<EditText android:id="@+id/editText" />
<EditText android:id="@id/editText" />

国際化対応メモ

アプリの国際化をしたのでその時の対応内容についてまとめました。

文字列の取得はR.swiftを利用する

R.swiftとは画像の取得・Storyboardの取得などを文字列を使わずに行えるようになるライブラリです。
導入する事で下のようにStoryboardやUIImageを取得できるようになります。

// StroryboardのViewControllerの取得
let viewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController() as? MyViewController
// ↓ のように書ける
let viewController = R.storyboard.main.initialViewController

// 画像の取得
let image = UIImage(named: "MyImage")
// ↓ のように書ける
let image = R.image.myImage

github.com

今回は文字列の取得にR.swiftを使いました。
R.swiftの導入をする事で下のように書くことができます。

NSLocalizedString("Main Close", comment: "")
// ↓ のように書ける
R.string.localizable.mainClose()

Storyboardの国際化は利用しない

翻訳ファイルを1箇所にまとめたかったので今回はStoryboardの国際化機能を利用しませんでした。
翻訳はLocalizable.stringsにまとめて下のように各画面のviewDidLoadなどでセットする方針にしています。

title = R.string.localizable.mainTitle()
bodyLabel.text = R.string.localizable.mainBody()

コードを各量は増えるのですが一箇所にまとまっているおかげで翻訳作業がスムーズでした。

各言語用のSchemeを作成する

今回は下のように各言語用のSchemeを作成してテストしました。
下の場合、MyApp(en)を選んでアプリ起動すると英語がデフォルトになります。

f:id:llcc:20220127160423p:plain

Schemeの作成は下のManage Schemesから行います。

f:id:llcc:20220127160210p:plain

下画面で既存Schemeを複製します。

f:id:llcc:20220127160231p:plain

作成画面で名前とApp Languageを選択すれば完了です。

f:id:llcc:20220127160254p:plain

Schemeを作る事で端末の言語を変えずに翻訳のテストをすることが可能です。
ただアプリ名やiOSが出している言語(App Tracking Transparencyアラートなど)は翻訳されないので注意が必要です。

NSShowNonLocalizedStringsオプションについて

XcodeSchemeではNSShowNonLocalizedStrings YESというオプションをセットする事が可能です。
これをセットすると未翻訳の文字列が大文字になるので翻訳漏れを発見しやすくなります。

ただ、その言語でアプリをしないと発見できない(英語だけ翻訳漏れしてい場合は英語でアプリを起動しないと変化がない)ので気休め程度に考えておくのが良さそうです。

f:id:llcc:20220127160934p:plain

画像や色(Assets)の国際化について

XcodeではAssetsファイルの国際化も対応しています。

f:id:llcc:20220213135855p:plain

国際化は画面右下から言語を選択する事で実施可能です。

f:id:llcc:20220213144030p:plain

選択すると下のように選択言語用の画像を設定できるようになります。

f:id:llcc:20220213144108p:plain

参考URL

iOSアプリケーションの国際化と地域化 - クックパッド開発者ブログ

iOSアプリの国際化対応の勘所とTips集(Swift版) - Qiita

iOS15のAppearanceについて整理する

iOS15でAppearanceの仕様が変わったので詳しく調べてみました。

Appearanceとは

AppearanceとはUIKitの機能の一つで、デフォルトの色やフォントなどを設定できます。

例えばアプリ起動時に下の処理を入れるとナビゲーションバーが青色になります。

UINavigationBar.appearance().barTintColor = UIColor(red: 220/255, green: 0/255, blue: 0/255, alpha: 1)

f:id:llcc:20220122105831p:plain

これ以外にもUILabelのデフォルト文字サイズやUISwitchのデフォルトの色など様々な設定が可能です。

iOS15での変更点

iOS15では既存の方法ではUINavigationBarの色を正しく変更できません。
スクロール位置が一番上のときは下のように真っ白、それ以外の時は青色になります。

f:id:llcc:20220122110144p:plain

iOS15からは下のようにstandardAppearanceとscrollEdgeAppearanceにセットする必要があります。
barTintColorがbackgroundColorに変わっていたりするので、セットする際は注意が必要です。

let appearance = UINavigationBarAppearance()
appearance.backgroundColor = UIColor(red: 220/255, green: 0/255, blue: 0/255, alpha: 1)
UINavigationBar.appearance().standardAppearance = appearance
UINavigationBar.appearance().scrollEdgeAppearance = appearance

上の2つappearanceですが登場はiOS13です。
iOS13とiOS14ではstandardAppearanceだけセットすればUINavigationBar.appearance()のセットと同じような挙動になりました。

standardAppearanceとscrollEdgeAppearanceについて

scrollEdgeAppearanceはスクロール位置が一番上の時に使われるAppearanceでstandardAppearanceはそれ以外に使われるAppearanceです。
そのためscrollEdgeAppearanceだけセットするとスクロール位置が一番上の時だけ設定が反映されます。

それぞれについてのAppleの説明は下のとおりです。
下を見る限りscrollEdgeAppearanceを使わない場合は自動的にstandardAppearanceに見るようです。
そのため今後のアップデートでstandardAppearanceのみセットすれば良くなるかもしれません。

standardAppearance…Describes the appearance attributes for the navigation bar to use when it is displayed with its standard height.
scrollEdgeAppearance…Describes the appearance attributes for the navigation bar to use when an associated UIScrollView has reached the edge abutting the bar (the top edge for the navigation bar). If not set, a modified standardAppearance will be used instead.

その他

Appearanceですが上記以外に下の2つがあります。
こちらはナビゲーションバーが小さい時(画面を横向きにした時)に使われるAppearanceです。

compactAppearance
compactScrollEdgeAppearance

下のようにセットすると横向きの時だけナビゲーションバーが赤くなります。
compactScrollEdgeAppearanceだけはiOS15からなのでavailableで判定しています。

let appearance = UINavigationBarAppearance()
appearance.backgroundColor = UIColor(red: 65/255, green: 105/255, blue: 225/255, alpha: 1)
UINavigationBar.appearance().standardAppearance = appearance
UINavigationBar.appearance().scrollEdgeAppearance = appearance
let appearance2 = UINavigationBarAppearance()
appearance2.backgroundColor = UIColor(red: 225/255, green: 105/255, blue: 65/255, alpha: 1)
UINavigationBar.appearance().compactAppearance = appearance2
if #available(iOS 15.0, *) {
   UINavigationBar.appearance().compactScrollEdgeAppearance = appearance2
}

f:id:llcc:20220122114448p:plain

また、UILabelなどスクロールが関係ないものは今まで通り使うことができます。

UILabel.appearance().font = .systemFont(ofSize: 24)

【Kotlin】ContentProviderでGoogle Play services out of dateエラーが出たときの対処

ContentProviderでデータ取得しようとしたら下のようなエラーが出て失敗しました。

Google Play services out of date

調べたところGoogle Play Servicesのアップデートや不足しているSDKを入れる事で対処できるようです。

android studio: LogCatにWarning、Google Play services out of date. Requires 12211000 but found 11580470 | Ninton

Google Play services out of date - Qiita

自分の場合は1つ前のAPIバージョンでエミュレータを作成したら動くようになりました。

f:id:llcc:20220119105116p:plain

カレンダー形式のUIDatePickerの正しいサイズを計算する

UIDatePickerですがiOSから下のようなカレンダー形式が使えるようになりました。

f:id:llcc:20220115194554p:plain

コードは下のとおりです。

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        let datePicker = UIDatePicker()
        datePicker.preferredDatePickerStyle = .inline
        datePicker.center = view.center
        view.addSubview(datePicker)
    }
}

こちらですが下のようにwidthを広げてしまうと曜日や時間の部分が潰れるという問題があります。
今回はこれを解消するためにsystemLayoutSizeFittingというメソッドを使って正しい高さをセットしてみようと思います。

datePicker.frame.size.width = view.frame.width

f:id:llcc:20220115194908p:plain

systemLayoutSizeFittingとはUIViewのメソッドで、引数に合うSizeを返してくれます。
下のように引数に自身のViewのWidthを入れ、横の優先順位を上げる(defaultHighをセットする)ことで横幅いっぱいの時用のサイズを取得できます。

let targetSize = CGSize(width: view.frame.width, height: 0)
let size = datePicker.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .defaultHigh, verticalFittingPriority: .defaultLow) → (390.0, 397.3333333333333)

あとはdatePickerに取得したsizeをセットすれば曜日や時間部分が潰れなくなります。

datePicker.frame.size = size

f:id:llcc:20220115195555p:plain

datePickerModeを変更した場合、それにあった数字を計算してくれます。

print(datePicker.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .defaultHigh, verticalFittingPriority: .defaultLow)) // → (390.0, 397.3333333333333)
datePicker.datePickerMode = .date
print(datePicker.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .defaultHigh, verticalFittingPriority: .defaultLow)) // → (390.0, 365.0)

ちなみにカレンダー形式UIDatePickerの正しい高さはiOS15とiOS14で微妙に違います。
下がiOS14で高さを計測した時の数字です。

このようにOSのバージョンによって微妙にずれがあるのでできるだけsystemLayoutSizeFittingを使って計算するのが良いかと思います。

print(datePicker.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .defaultHigh, verticalFittingPriority: .defaultLow)) // → (390.0, 405.6666666666667)
datePicker.datePickerMode = .date
print(datePicker.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .defaultHigh, verticalFittingPriority: .defaultLow)) // → (390.0, 368.3333333333333)

ただsystemLayoutSizeFittingは既にDatePickerのサイズが違う状態では違う値になるので注意が必要です。
下のようにviewDidAppearでsystemLayoutSizeFittingを取得し直したらtargetSizeより大きいWidthになってしまいました。
もしviewDidAppear時点でDatePickerのWidthが初期値(320)だと正しい数字が取れるようです。

class ViewController: UIViewController {
    let datePicker = UIDatePicker()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        datePicker.preferredDatePickerStyle = .inline
        let targetSize = CGSize(width: view.frame.width, height: 0)
        datePicker.datePickerMode = .date
        let size = datePicker.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .defaultHigh, verticalFittingPriority: .defaultLow)
        datePicker.frame.size = size
        datePicker.center = view.center
        view.addSubview(datePicker)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let targetSize = CGSize(width: view.frame.width, height: 0)
        print(datePicker.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: .defaultHigh, verticalFittingPriority: .defaultLow)) // → (407.0, 377.0)
    }
}

Embedded binary is not signed with the same certificate as the parent appエラーの対処法

iOSアプリ開発で下のエラーが出たので対処しました。
エラーは実機検証のときのみ発生して、シミュレータでは発生しませんでした。

エラーを見た所アプリ本体とWidget Extensionの証明書が違うとの事です。

error: Embedded binary is not signed with the same certificate as the parent app. Verify the embedded binary target's code sign settings match the parent app's.

        Embedded Binary Signing Certificate:    Not Code Signed
        Parent App Signing Certificate:     Apple Development: XXX

今回の原因はウィジェットのExcluded Architectureにarm64に入れていた事でした。

f:id:llcc:20220114214717p:plain

arm64は下のようにシミュレータだけに入れる必要があります。
arm64はスマホで使われるアーキテクチャなので、これを除外することでウィジェットの実機ビルドがうまくいかなかったのかと思われます。

f:id:llcc:20220114214832p:plain

調べた所signing & capabilitiesのTermを選択していない時にもこのエラーは出るようです。
その場合は正しいTermを選択すれば解消できます。

f:id:llcc:20220114215503p:plain

MacBook Pro 2021でのビルド時間測定

MacBook Proを買い替えたのでXcodeでのビルド時間やAdobe Premiereなどでの動画書き出し時間を比較してみました。

新旧マシンのスペックは下の通りです。
ビルド時間を短くしたかったので、どちらもスペック高めのものを購入しました。

■ 新MacBook Pro
MacBook Pro 2021 16インチ
CPU、GPUApple M1 Max(10コアCPU、32コアGPU、16コアNeural Engine搭載)
メモリ…64GBユニファイドメモリ

■ 旧MacBook Pro
MacBook Pro 2019 16インチ
CPU…第9世代の2.4GHz 8コアIntel Core i9プロセッサ(Turbo Boost使用時最大5.0GHz)
GPUAMD Radeon Pro 5500M(8GB GDDR6メ‍モ‍リ搭載)
メモリ…32GB 2,666MHz DDR4メモリ

比較1 Xcodeでプロジェクトのビルド1

XcodeiPhoneアプリのClean後のビルド時間を測定しました。
プロジェクトはSwiftファイルが100ファイルほどでCocoaPodsでライブラリ20個程入れています。
3回測定して平均を計算しました。
()内は各ビルド時間です。

旧…69.1秒(71.2秒、69.7秒、66.3秒) 新…41.0秒(39.9秒、38.5秒、44.6秒)

ビルド時間は40.7%削減。

比較2 Xcodeでプロジェクトのビルド2

別のプロジェクトでも試してみました。
こちらはファイル数は150くらいでライブラリは20個ほど入れています。

旧…147.5秒(148.8秒、145.2秒、148.4秒) 新…45.7秒(45.7秒、45.4秒、46.1秒)

ビルド時間は69.0%削減。

比較3 Adobe Premiereで動画書き出し

動画の書き出しも試してみました。
40分くらいの動画でエフェクトはほぼないです。
出力形式はH.264でプリセットYoutube 2160p 54K Ultra HDを選択しました。

旧…37分17秒(38分47秒、38分36秒、34分29秒) 新…6分9秒(6分5秒、05分54秒、6分28秒)

書き出し時間85.8%削減。

まとめ

計測してみましたがビルド時間がかなり改善したので購入して良かったと思います。
動画についてはここまで差が出ると思ってなかったので驚きました。