マネーフォワード MEのiOS 15対応

こんにちは。
マネーフォワード MEのiOSエンジニア、ひらた( @cafedeichi )です。

ここ数年、マネーフォワード MEは比較的早い段階で新しいXcodeへの開発環境の移行とiOSへの対応をしています。主な理由は、古いXcodeで開発を進めると、それだけ新しいものへ移行した時の技術負債が増え、本来集中すべきサービスの施策の遂行等に影響を及ぼしていく可能性が十分にあるからです。また、通常、翌年度末には古いXcodeでビルドしたアプリのAppleへの審査は申請できなくなるので、実施するのであれば早い方が望ましいと考えています。
今年もXcode 13およびiOS 15のbeta 1から動作検証をはじめ、いくつかの問題への対応と新機能の追加を行い、先日iOS 15に対応したバージョンをリリースしました。この投稿は動作検証から実装までのまとめです。これからiOS 15対応を検討されている方の参考となれば幸いです。
なお、この内容はXcode 13 beta 5、iOS 15 beta 8で確認した結果を元にしているため、それらの後続のバージョンでは挙動が変わっている可能性があります。実装はこの投稿用に編集しているため、実際のものとは異なります。

問題への対応

UINavigationBar、UITabBarの背景色が消える

まずは既存実装を変えずにXcode 13 beta 1でビルドして、最初に目に入ったのが背景色の問題です。ビルドは通ったものの、UINavigationBarとUITabBarの背景色が消えました。UIKitの仕様変更によるもので、それらのisTranslucentがfalseとなっているのが主な原因でした。

While UIKit does its best to make this new appearance seamless in your app, there are a few issues you may encounter. You should audit your code for places where you may be setting a bar’s translucent property to false and check for any UIViewControllers that have non-standard edgesForExtendedLayout. Both of these conditions will cause visual issues with the new appearance.
(What’s new in UIKit – WWDC21)

この問題はUINavigationBarAppearanceを使うことで解決します。Apple Developer ForumsでAppleのエンジニアが実装例を示していたので、それに倣いました。
iOS 15では、画面のスクロール中と、スクロールが終端に達した時のUINavigationBarの背景色が変わるので、standardAppearancescrollEdgeAppearanceの設定を同じにすることで、従来通りの見た目に戻ります。
UINavigationBarAppearanceはiOS 13からの機能なので、iOS 14でもそのまま適用できます。

let appearance = UINavigationBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = UIColor.systemBackground
navigationBar.standardAppearance = appearance
navigationBar.scrollEdgeAppearance = navigationBar.standardAppearance

UITabBarにおいてもiOS 15からscrollEdgeAppearanceが利用可能になったので、同様の方法で問題を解決できます。

let appearance = UITabBarAppearance()
appearance.configureWithOpaqueBackground()
appearance.backgroundColor = UIColor.systemBackground
tabBar.standardAppearance = appearance

if #available(iOS 15.0, *) {
    tabBar.scrollEdgeAppearance = tabBar.standardAppearance
}

UITableViewの上部に余白が生じる

 

いくつかの画面で、UITableViewの上(UINavigationBarの下)に余白が生じている箇所がありました。こちらもUIKitの仕様変更によるものです。

We have a new appearance for headers in iOS 15. For plain lists, section headers now display seamlessly in line with the content, and only display a visible background material when becoming pinned to the top as you scroll down. In addition, there’s new padding inserted above each section header to visually separate the sections with this new design.
(What’s new in UIKit – WWDC21)

Plain StyleのUITableViewに対しては、iOS 15から利用可能となったsectionHeaderTopPaddingを0にして余白をなくすことで従来のデザインに戻しました。

if #available(iOS 15.0, *) {
    tableView.sectionHeaderTopPadding = 0
}

Grouped StyleのUITableViewの上部も余白が生じていましたが、sectionheadertoppaddingの設定が有効ではなかったので、tableHeaderViewに高さがほぼ0(leastNonzeroMagnitude)のUIViewを設定しました。

tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: CGFloat.leastNonzeroMagnitude))        

UILabelのAutoShrinkが早く発動する

iOS 14.7.1 iOS 15.0

一部の画面で従来よりもフォントが小さくなっている箇所がありました。上記の例の場合、「今月の家計簿」の「収入」「支出」や、「今月の予算(変動費)」の「残り」等が該当します。これはUILabelのadjustsFontSizeToFitWidthのtrueになっている場合に起きる問題で、iOS 14以前よりもAutoShrinkの発動が早くなっていることが考えられました。よってUILabelを使っている箇所を全般的に見直し、heightを少し大きめにとることで従来通りの見た目となるようにしました。

safeAreaLayoutGuideを使っていない

safeAreaLayoutGuideを使っていなかったことで、一部のVIewがUINavigationBarまたはUITabBarの裏に隠れるようになってしまいました。これは、UINavigationBarのisTranslucentをtrueにしたことで起きた問題です。
この問題は特にiOS 15だからというわけではありませんが、マネーフォワード MEのように比較的長い歴史を持ち、かつisTranslucentをfalseにしてきたアプリのiOS 15対応では、よく起きる事象ではないかと考えられます。

// ×:isTranslucent = true の場合、ViewはNavigationBarの下に隠れてしまう。
NSLayoutConstraint.activate([
    errorHeaderView.topAnchor.constraint(equalTo: view.topAnchor),
    errorHeaderView.leftAnchor.constraint(equalTo: view.leftAnchor),
    errorHeaderView.rightAnchor.constraint(equalTo: view.rightAnchor),
    errorHeaderView.heightAnchor.constraint(equalToConstant: errorHeaderView..headerErrorViewHeight)
])

// ○:isTranslucentの状態に関わりなく、Viewは適切な箇所に表示される。
let guide = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
    errorHeaderView.topAnchor.constraint(equalTo: guide.topAnchor),
    errorHeaderView.leftAnchor.constraint(equalTo: guide.leftAnchor),
    errorHeaderView.rightAnchor.constraint(equalTo: guide.rightAnchor),
    errorHeaderView.heightAnchor.constraint(equalToConstant: errorHeaderView.headerErrorViewHeight)
])

新機能の追加

Widget Suggestionsの導入

iOS 15では、ウィジェットのIntelligenceが強化されています。iOS 14からあったSmart Rotateは、Smart Stackに追加済みのウィジェットをユーザーにとって適切なタイミングで表示する仕組みでしたが、iOS 15から登場したWidget Suggestionsは、ユーザーが使用していないウィジェットもSmart Stackに「提案」という形で表示させることができます。つまり、普段は使われていない、あるいはその存在も知られていないウィジェットをユーザーが目にすることができます。

iOS 15 introduces a new feature called Widget Suggestions, a brand-new way for users to discover your widget and receive proactive and relevant information.
For Smart Stacks with Widget Suggestions enabled, the system can insert a new widget into a stack that doesn’t already contain it.
(Add intelligence to your widgets – WWDC21)

マネーフォワード MEでは、クイックアクションウィジェットにWidget Suggestionsを導入することにしました。Widget Suggestionsをサポートするには、INInteraction、またはINRelevantShortcutを使う2つの方法があります。どちらかひとつでも良いのですが、提案をされる保証はなく、システム次第となっています。よってシステムにより多くのヒントを与えてウィジェットが提案される機会を増やすためには、両方とも使用するのが望ましいです。

INInteraction

INInteractionは、システムにユーザーの行動パターンを学習させ、ウィジェットを提案するよう仕向けることができます。実装はSiri Shortcutsと同じ方法でできます。実際、システムにdonateされたintentは、Siri Suggestionsとしても表示されることになります。

Not only do the INInteraction donations you make enable Smart Rotations and Widget Suggestions, but they also allow the system to show your intent as a Siri Suggestion on the Lock Screen, in Spotlight, and in the Siri Shortcut Suggestions widget.
(Add intelligence to your widgets- WWDC21)

let intent = QuickActionIntent()

intent.intentItem = IntentItem(identifier: "\(model.id)", display: model.displayString)
intent.intentItem?.amount = INCurrencyAmount(amount: NSDecimalNumber(value: model.amount), currencyCode: "JPY")
intent.intentItem?.categoryId = NSNumber(value: model.middleCategoryId)

let interaction = INInteraction(intent: intent, response: nil)

interaction.donate { error in
    if let error = error as NSError? {
        Swift.debugPrint(error.localizedDescription)
    }
}

INRelevantShortcut

INRelevantShortcutは、ウィジェットの提案をいつ行うべきかをシステムに伝えることができます。以下の実装例は、朝と夜の時間帯に毎日の日課としてウィジェットを提案しています。

let intent = QuickActionIntent()
...

guard let shortcut = INShortcut(intent: intent) else {
    return
}
let suggestedShortcut = INRelevantShortcut(shortcut: shortcut)

suggestedShortcut.relevanceProviders = [
    INDailyRoutineRelevanceProvider(situation: .morning),
    INDailyRoutineRelevanceProvider(situation: .evening)
]

if #available(iOS 15.0, *) {
    suggestedShortcut.widgetKind = "QuickActionWidget"
}

INRelevantShortcutStore.default.setRelevantShortcuts([suggestedShortcut]) { error in
    if let error = error as NSError? {
        Swift.debugPrint(error.localizedDescription)
    }
}

まとめ

以上、マネーフォワード MEのiOS 15対応についてお話しました。iOS 13、14の時と比べて大きな問題はなく、Xcode 13がbeta 1の時点でビルドが通ったことからも、iOS 15対応は円滑に行うことができた印象があります。
Widget Suggestionsは少ないコードで簡単に対応することができました。次回、新規でウィジェットを開発する機会があれば、リリースに合わせてWidget Suggestionsも導入したいと考えています。

最後までお読みいただきありがとうございました。


マネーフォワードでは、エンジニアを募集しています。
ご応募お待ちしています。

【サイトのご案内】
マネーフォワード採用サイト
Wantedly
京都開発拠点

【プロダクトのご紹介】
お金の見える化サービス 『マネーフォワード ME』 iPhone,iPad Android

ビジネス向けバックオフィス向け業務効率化ソリューション 『マネーフォワード クラウド』

おつり貯金アプリ 『しらたま』

お金の悩みを無料で相談 『マネーフォワード お金の相談』

だれでも貯まって増える お金の体質改善サービス 『マネーフォワード おかねせんせい』

金融商品の比較・申し込みサイト 『Money Forward Mall』

くらしの経済メディア 『MONEY PLUS』

Pocket