酢ろぐ!

カレーが嫌いなスマートフォンアプリプログラマのブログ。

DevelopersIOで2024年3月に執筆した記事

技術系の記事はDevelopersIOで執筆することにした。一方、すぐに修正されそうなトピックやワークアラウンド的な内容は記事の寿命が短くなる可能性があるため、引き続き「酢ろぐ!」で書くことにしている。

毎月の終わりには、DevelopersIOで執筆した記事へのリンクをまとめる。以下は2024年3月に執筆した記事の一覧である。

Xcode 15.3 を使って今のうちに Swift 6 へのアップデートに備える

Xcode 15.3を使用したSwift 6への準備方法を紹介した。Swift 6の新機能を有効にする手順を書いた。Build SettingsでSwift Compilerの設定を変更し、生じる可能性のあるエラーやワーニングに対応する内容を示す。

dev.classmethod.jp

3月13日から始まったプライバシーマニフェスト未対応の警告メールを受け取った

2024年3月13日から適用されたApp Storeの新しいプライバシー要件に関する実験的な検証を紹介した。5月1日から厳密化される新しいプライバシー要件では仕様が変わる可能性もあり得る。

dev.classmethod.jp

Swift Chartsを使って線グラフを表示する

iOS 16以降で利用可能なSwift Chartsを使用して線グラフを表示する方法を紹介した。基本的な実装方法、Y軸の表示範囲指定、Y軸ラベルの追加、実際のデータを使ったグラフ表示などをコード例と共に紹介している。

dev.classmethod.jp

その他

  • 2023年 1月 2月 3月 4月 5月 6月 7月 8月 9月 10月 11月 12月
  • 2024年 1月 2月 3月 4月 5月 6月 7月 8月 9月 10月 11月 12月

DevelopersIOで2024年1月に執筆した記事

技術系の記事はDevelopersIOで執筆することにした。一方、すぐに修正されそうなトピックやワークアラウンド的な内容は記事の寿命が短くなる可能性があるため、引き続き「酢ろぐ!」で書くことにしている。

毎月の終わりには、DevelopersIOで執筆した記事へのリンクをまとめる。以下は2024年1月に執筆した記事の一覧である。

CocoaPodsでOpenCVをiOSアプリに導入する

CocoaPodsを使用してiOSアプリにOpenCVを導入する方法を紹介した。OpenCVは画像処理やコンピュータビジョンのためのライブラリである。簡単な例として、UIImageオブジェクトをOpenCV形式に変換し、グレースケール化などの処理を行う手順を解説した​​​​​​​​​​。

dev.classmethod.jp

最新のOpenCVをiOSアプリで使いたい! opencv2.xcframework を自分でビルドする

CocoaPods経由でOpenCVをインストールするとバージョンが古いことがわかった。最新のOpenCVをiOSアプリで使用するために、自分でビルドする方法を紹介した。

dev.classmethod.jp

サードパーティSDKのPrivacy Manifests対応状況を調査した

iOSアプリのサードパーティSDKがPrivacy Manifestsにどの程度対応しているかを調査した内容をまとめた。2024年春までに対応が必要なSDKのプライバシーマニフェスト対応がどのくらい進んでいるかリスト化した。Firebase iOS SDKの対応が完了するくらいまでは続けていきたいと考えている。

dev.classmethod.jp

AppFollow と Slack を使ってアプリのレビューをリアルタイムに受け取る

ビューをリアルタイムで受け取る方法を紹介した。AppFollowはアプリストアの分析に特化したツールで、Google Playストア、App Store、Microsoft Storeなどのレビューを一元管理できる。Slackと統合することで

dev.classmethod.jp

その他

  • 2023年 1月 2月 3月 4月 5月 6月 7月 8月 9月 10月 11月 12月
  • 2024年 1月 2月 3月 4月 5月 6月 7月 8月 9月 10月 11月 12月

DevelopersIOで2023年12月に執筆した記事

技術系の記事はDevelopersIOで執筆することにした。一方、すぐに修正されそうなトピックやワークアラウンド的な内容は記事の寿命が短くなる可能性があるため、引き続き「酢ろぐ!」で書くことにした。

毎月の終わりには、DevelopersIOで執筆した記事へのリンクをまとめる。以下は2023年12月に執筆した記事の一覧である。

SwiftUIで日本語テキストが不自然に改行される

SwiftUIでTextビュー内の日本語テキストが不自然に改行される問題を解説した。この問題を解決するために「Word Joiner」(U+2060)や「Zero Width Space」(U+200B)などの特殊文字を使用し、さまざまな画面サイズやレイアウトで適切にテキストが改行されるようなテクニックを紹介した。

SwiftUIでカスタムダイアログを実装したが閉じる時にアニメーションしない

SwiftUIでカスタムダイアログを実装した際の、閉じる時のアニメーションがない問題について解説した。この問題を解決するために、.zIndex プロパティを使ってダイアログをビュー階層の最前面に配置し、フェードアウトアニメーションが正しく表示されるようにした方法を紹介した。

SwiftUIでスクリーンショットの撮影時にアラートを表示する

SwiftUIでスクリーンショット撮影時にアラートを表示する方法を解説した。UIApplication.userDidTakeScreenshotNotification を利用してスクリーンショット撮影を検知し、ユーザーにメッセージを表示する手法を紹介した。

dev.classmethod.jp

2024年春以降、Privacy Manifests未対応のiOSアプリはリジェクトされてしまう

2024年春以降、AppleがPrivacy Manifestsに対応していないiOSアプリの新規申請とアップデートをリジェクトすることを発表した。この記事では、Privacy Manifestsがアプリのユーザーデータの収集・使用方法を明示する要件であり、アプリ開発者がユーザーデータの収集・使用の詳細を定義しプライバシーレポートを生成する必要がある点、サードパーティSDKの対応も必要であることを紹介した。

dev.classmethod.jp

その他

  • 2023年 1月 2月 3月 4月 5月 6月 7月 8月 9月 10月 11月 12月
  • 2024年 1月 2月 3月 4月 5月 6月 7月 8月 9月 10月 11月 12月

DevelopersIOで2023年11月に執筆した記事

技術系の記事はDevelopersIOで執筆することにした。一方、すぐに修正されそうなトピックやワークアラウンド的な内容は記事の寿命が短くなる可能性があるため、引き続き「酢ろぐ!」で書くことにした。

毎月の終わりには、DevelopersIOで執筆した記事へのリンクをまとめる。以下は2023年11月に執筆した記事の一覧である。

SlackでRSSフィードを購読する

SlackでRSSフィードを購読する手順を説明した。RSSフィードURLを取得後、SlackにRSSアプリを追加し設定することで、新しい記事がSlackチャンネルに通知される。

SSHエージェントとして 1Password を使うと、GitHub上で Unverified と表示される

1PasswordをSSHエージェントとして使用時、GitHubで「Unverified」と表示される問題が発生した。認証キーと署名キーをGitHubに登録することで「Verified」表示に変わった。

Keynoteを使ってアイキャッチを作成する

Keynoteを使ってブログやSNS用のアイキャッチ画像を作る方法を紹介した。背景にタイトルを加えて簡単にカスタマイズし、効果的なアイキャッチを作成できた。

その他

  • 2023年 1月 2月 3月 4月 5月 6月 7月 8月 9月 10月 11月 12月
  • 2024年 1月 2月 3月 4月 5月 6月 7月 8月 9月 10月 11月 12月

お手軽多言語対応!GoogleドキュメントのスプレッドシートからiOSとAndroidの文字列リソースを生成 (2023年10月版)

多言語対応アプリの開発は非常に大変だ。特に、アプリ内で使用される各種テキストを管理する「文字列リソース」の取り扱いについては、更新や検証に多大な労力を要する。この課題を解決するために、csv2strings というスクリプトを公開した。

csv2strings は、Googleドキュメントのスプレッドシートを利用し、iOS用の Localizable.strings と Android用の strings.xml というローカライズファイルを自動生成するRuby製のツールだ。

github.com

2019年7月、前回のブログ記事で、同様のツールを紹介した。しかし、旧バージョンのツールはSwiftを使っているため、コードの変更ごとにコンパイルが必要で、編集作業が煩雑だった。

新しく公開したRuby版の利点は、コードの修正が容易であること、そしてmacOSだけでなく他のOSでも動作する点にある。特にCIサービスでは、Androidアプリの開発環境としてよくUbuntuが使用されるが、以前のmacOS専用のバイナリ形式はこの環境では動かなかった。

iOSアプリ開発に不可欠なライブラリ管理ツール「CocoaPods」はRubyで動作するため、多くの開発者は既にRuby環境を構築済みである。Ubuntu上でもRubyは標準的に利用できるため、追加で特別なインストールを行う必要はない。

文字列リソースの自動生成の背景

そもそも文字列リソースの自動生成を始めた背景としては、あるプロジェクトで対応する言語が急に7つに増えた経験にある。

もともと日本語のみ対応のアプリだったものが、国際市場に進出するため、英語、韓国語、中国語(繁体・簡体)、ドイツ語、イタリア語、フランス語にも対応することになった。

それまでは、Localizable.stringsstrings.xml の作成を手動で行っていたが、それらの文言を外部の翻訳サービスに依頼し、エクセルシートで受け取った後、一つ一つ手作業でコピー&ペーストする作業は、効率が悪くミスも多発していた。

特に英語以外の言語での区別が難しく、コピペミスが起こりやすい状況だったため、チームでアクセス可能なGoogleドキュメントのスプレッドシートに情報を集約し、そこからiOS/Androidのローカライズファイルを自動生成する方法を導入した。

ツールの使用方法

「csv2strings」は、Google Docsのスプレッドシートに記入された情報をcsv形式でダウンロードし、それを基に Localizable.stringsstrings.xml を生成する。機械的に実行されるため、作業の効率化に寄与しエラーを減らすことができる。

具体的な使用例を以下に示す。Google Docsに更新されたテキストを含むcsvファイルをダウンロードし、それを使用してローカライズファイルを生成、そしてアプリの対応するディレクトリにそれらのファイルをコピーしている。

# Google Docsからcsvをダウンロードする
curl -L "https://docs.google.com/spreadsheets/{GUID}&output=csv" -o "${ENV_RESOURCES_DIR}strings/string.csv"
sleep 5

# csvから文字列リソースを生成
ruby csv2strings "${ENV_RESOURCES_DIR}strings/string.csv" "${ENV_RESOURCES_DIR}strings/"

# アプリへ展開
cp $ENV_RESOURCES_DIR/strings/ios/ja/Localizable.strings $PROJECT_DIR/ios/ptcgnote/Resource/ja.lproj/Localizable.strings

このスクリプトを使用することで、文言の変更や多言語対応が機械的に、かつ効率的に行えるようになり、アプリ開発のプロセスが大幅に簡素化できた。

関連記事

過去の投稿で紹介したcsv2stringsの初期バージョン。

blog.ch3cooh.jp

RevenueCatで `The receipt is not valid.` エラーが発生して、共有シークレットを更新してもエラーが解決できなかった

アプリ開発界隈で、課金管理をスムーズに行うために多くの開発者が利用する「RevenueCat」だが、導入する際に「The receipt is not valid.(レシートが無効です)」というエラーが発生する。

これは大抵の場合、「アプリ固有の共有シークレット」が間違って設定されているか、設定自体がなされていないことが原因である。

SwiftyStoreKitからRevenueCatへの移行の経緯

現在、SwiftyStoreKitからRevenueCatへの移行作業に取り組んでいる。

Xcode 14.0でのSwiftyStoreKitが動かなくなる問題で苦労したため、新規の案件ではSwiftyStoreKitを使うのを避けている。Swift Concurrency に対応したStoreKit 2や、StoreKit 1をラッピングしたRevenueCatなどのサービスを利用している。今回はRevenueCatを選択した。

RevenueCatを使っていて嬉しい理由として、コードが非常にシンプルであることだ。以下にサブスクリプションの復元処理(リストア)の一例を挙げる。

private func restoreSubscription() async -> Result<Void, Error> {
    do {
        let purchaserInfo = try await Purchases.shared.restorePurchases()
        if let entitlementInfo = purchaserInfo.entitlements.all["premium"] {
            if entitlementInfo.isActive {
                logger.debug("課金中: \(String(describing: entitlementInfo.expirationDate)) まで")
            } else {
                logger.debug("レシートはあるが有効期間切れで非課金")
            }
        } else {
            logger.debug("レシートがないので非課金")
        }
        return .success(())
    } catch {
        return .failure(error)
    }
}

The receipt is not valid.エラーと解決方法

しかし、復元処理中にThe receipt is not valid.というエラーが発生し、解決までに少々手間取った。この問題は「アプリ固有の共有シークレット」が適切に設定されていないときに起こることが多い。

共有シークレットの設定については、RevenueCatの公式ドキュメントで詳細に説明されている。適切に設定し直したはずなのにエラーが解消されない場合もある。

www.revenuecat.com

Chromeで設定が上手くいかないケース

調査したところ、Chromeを使用してRevenueCatの設定を更新すると、変更が正しく保存されないという問題を発見した。

もしThe receipt is not valid.エラーが続いている場合、ブラウザをSafariに変えて設定を更新すると良いかもしれない。

関連記事

zenn.dev

`bundle exec pod update` 実行時に `bundler: failed to load command: pod` エラーが発生する

2023年10月7日の activesupport のアップデートによって CocoaPods が使えなくなった。すぐに修正されると思うが、直近で困ってる方のために備忘録を残す。

bundler: failed to load command: pod エラーが発生する

bundle update のあと、 bundle exec pod update の実行時に、bundler: failed to load command: pod と CocoaPods の読み込みに失敗するエラーが発生した。

$ bundle exec pod update                        
bundler: failed to load command: pod (/PATH/works/ptcgnote_app/ptcgnote/vendor/bundle/ruby/2.7.0/bin/pod)
Traceback (most recent call last):
    24: from /PATH/.rbenv/versions/2.7.4/bin/bundle:25:in `<main>'
  23: from /PATH/.rbenv/versions/2.7.4/bin/bundle:25:in `load'
   22: from /PATH/.rbenv/versions/2.7.4/lib/ruby/gems/2.7.0/gems/bundler-2.4.20/exe/bundle:29:in `<top (required)>'
  21: from /PATH/.rbenv/versions/2.7.4/lib/ruby/site_ruby/2.7.0/bundler/friendly_errors.rb:117:in `with_friendly_errors'
    20: from /PATH/.rbenv/versions/2.7.4/lib/ruby/gems/2.7.0/gems/bundler-2.4.20/exe/bundle:37:in `block in <top (required)>'
  19: from /PATH/.rbenv/versions/2.7.4/lib/ruby/site_ruby/2.7.0/bundler/cli.rb:28:in `start'
   18: from /PATH/.rbenv/versions/2.7.4/lib/ruby/site_ruby/2.7.0/bundler/vendor/thor/lib/thor/base.rb:485:in `start'
  17: from /PATH/.rbenv/versions/2.7.4/lib/ruby/site_ruby/2.7.0/bundler/cli.rb:34:in `dispatch'
    16: from /PATH/.rbenv/versions/2.7.4/lib/ruby/site_ruby/2.7.0/bundler/vendor/thor/lib/thor.rb:392:in `dispatch'
  15: from /PATH/.rbenv/versions/2.7.4/lib/ruby/site_ruby/2.7.0/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command'
   14: from /PATH/.rbenv/versions/2.7.4/lib/ruby/site_ruby/2.7.0/bundler/vendor/thor/lib/thor/command.rb:27:in `run'
  13: from /PATH/.rbenv/versions/2.7.4/lib/ruby/site_ruby/2.7.0/bundler/cli.rb:492:in `exec'
    12: from /PATH/.rbenv/versions/2.7.4/lib/ruby/site_ruby/2.7.0/bundler/cli/exec.rb:23:in `run'
  11: from /PATH/.rbenv/versions/2.7.4/lib/ruby/site_ruby/2.7.0/bundler/cli/exec.rb:58:in `kernel_load'
   10: from /PATH/.rbenv/versions/2.7.4/lib/ruby/site_ruby/2.7.0/bundler/cli/exec.rb:58:in `load'
   9: from /PATH/works/ptcgnote_app/ptcgnote/vendor/bundle/ruby/2.7.0/bin/pod:25:in `<top (required)>'
     8: from /PATH/works/ptcgnote_app/ptcgnote/vendor/bundle/ruby/2.7.0/bin/pod:25:in `load'
   7: from /PATH/works/ptcgnote_app/ptcgnote/vendor/bundle/ruby/2.7.0/gems/cocoapods-1.13.0/bin/pod:36:in `<top (required)>'
    6: from /PATH/.rbenv/versions/2.7.4/lib/ruby/site_ruby/2.7.0/rubygems/core_ext/kernel_require.rb:38:in `require'
   5: from /PATH/.rbenv/versions/2.7.4/lib/ruby/site_ruby/2.7.0/rubygems/core_ext/kernel_require.rb:38:in `require'
     4: from /PATH/works/ptcgnote_app/ptcgnote/vendor/bundle/ruby/2.7.0/gems/cocoapods-1.13.0/lib/cocoapods.rb:9:in `<top (required)>'
   3: from /PATH/.rbenv/versions/2.7.4/lib/ruby/site_ruby/2.7.0/rubygems/core_ext/kernel_require.rb:38:in `require'
    2: from /PATH/.rbenv/versions/2.7.4/lib/ruby/site_ruby/2.7.0/rubygems/core_ext/kernel_require.rb:38:in `require'
   1: from /PATH/works/ptcgnote_app/ptcgnote/vendor/bundle/ruby/2.7.0/gems/activesupport-7.1.0/lib/active_support/core_ext/array/conversions.rb:8:in `<top (required)>'
/PATH/works/ptcgnote_app/ptcgnote/vendor/bundle/ruby/2.7.0/gems/activesupport-7.1.0/lib/active_support/core_ext/array/conversions.rb:108:in `<class:Array>': undefined method `deprecator' for ActiveSupport:Module (NoMethodError)
Did you mean?  deprecate_constant

エラーメッセージの内容は完全には理解できないが、ActiveSupport ライブラリ内で deprecator というメソッドが未定義であることが記載されている。 deprecatordeprecate_constant に名前が変更されたのかな?

解決方法

Gemfile.lock を確認したところ、activesupport のバージョンが v7.0.8 から v7.1.0 にアップデートされていることを確認した。このアップデートが破壊的な変更を含んでいるのであろう。

---    activesupport (7.0.8)
+++    activesupport (7.1.0)

問題発生の原因がわかった。影響範囲が広い(どの環境でも発生しうる)問題のため、そのうち CocoaPods 側でも対応してくれることだろう。

暫定的な対策としては、Gemfile を編集して activesupport のバージョンを 7.0.8 で固定すると、問題なく CocoaPods が使えるようになる。

source "https://rubygems.org"

gem 'cocoapods'
gem 'fastlane'
gem 'activesupport', '= 7.0.8'

Gemfile を弄れない環境の場合、Bundler の更新は止めておいた方が良さそうだ。