カルパス食べたい

日々の色々を書きます

iOS の標準マップが知らない間に使いやすくなっていた気がする

自分は北海道の田舎出身なので電車にあまり慣れていません。
学生の時東京に初めて一人で乗り込んだ時に電車わからなさすぎたので iOS の標準マップを使って頑張ろうとした記憶があります。
ただ、その時の標準マップは多分自分にとっては使いにくかったのか Google マップをインストールして、最近までずっと使っていました。
最近のことなのに何がきっかけかは覚えていないですが、iOS の標準マップ使ってみるか〜と思って、使ってみたら自分にとって使いやすくなっていたので、こんなアプリ作りたいなという意味でもメモしておこうと思いました。

自分にとって iOS 標準マップの何が良かったのか

電車に慣れていない自分が一番電車で迷うポイントは、「次どの電車に乗り換えればいいんや...?」というものになります。
ちなみに、現状の Google マップは ↓ のような感じです。

f:id:kalupas:20201101101346p:plain:w200

十分使いやすいものではあったのですが、自分が一番困る「次何番線に乗れば?」という気持ちに対しては、小さく駅の情報の下に書かれているだけでパッとは判断しにくかったのが難点でした。

iOS のマップを久しぶりに触ってみたら ↓ のようになっていました。

f:id:kalupas:20201101101640p:plain:w200

各駅を出た後、次何番線に乗り換えれば良いかがめっちゃスッと入ってくる!!
と思いました。細かい話ではありますが、こんな感じのアプリを自分も作ってみたいなあと思いました。

せっかくなので HIG の話も絡めます

ただ良いアプリだな〜で終わってしまってはもったいないのと、最近 Human Interface Guidelines で読んでいた章の中に iOS の標準マップについて言及されているものがあったので、そちらについても触れておきます。

自分が読んだのは HIG の Color の章になります。

developer.apple.com

この中では、マップアプリについて ↓ のように言及されていました。

Consider how artwork and translucency affect nearby colors. Variations in artwork sometimes warrant changes to nearby colors in order to maintain visual continuity and prevent interface elements from becoming overpowering or underwhelming. Maps, for example, displays a light color scheme when using map mode but switches to a dark color scheme when satellite mode is activated. Colors can also appear different when placed behind a translucent element, or when applied to a translucent element, such as a toolbar.

DeepL さんで翻訳してあげると ↓ になります。

アートワークと半透明度が近隣の色にどのように影響するかを検討してください。アートワークの多様性は、視覚的な連続性を維持し、インターフェイス要素が威圧的になったり威圧的になったりするのを防ぐために、近くの色を変更する必要がある場合があります。たとえば、地図は、地図モードを使用しているときは明るい配色で表示されますが、衛星モードがアクティブになると暗い配色に切り替わります。また、半透明の要素の後ろに配置したり、ツールバーなどの半透明の要素に適用したりすると、色が異なって見えることがあります。

何を言っているのか、実際にアプリの画像をみるとわかりやすいと思うので貼ります。

f:id:kalupas:20201101102336p:plain:w200f:id:kalupas:20201101102347p:plain:w200

こんな感じで、地図モードだと明るい配色ですが、衛星モードだと暗い配色になっていることがわかると思います。
細かい違いですが、iOS アプリを作っている自分としては、こんな細かいところに気を遣えるの凄いな... と思いました。
確かに、もし衛星モードで白いセミモーダルが出ていたとするなら、威圧的というか若干目立ち過ぎてしまう感じはあるなと言われてみてですが思いました。

自分もこのようなアプリを作ってみたいなとしみじみ思いました。

Xcode12 対応できてないけど Split View でシミュレータを利用するための方法

自分が所属している会社では、やっと Xcode12 対応が始まりそうです。
しかし、まだ Xcode11.x を利用している状況であり、個人的に SwiftUI を触っている自分は Xcode12 の Split View でシミュレータを並べて表示できる機能(参照)が早く欲しいという気持ちでした。

なんと Xcode11 を使いながらシミュレータのその機能だけを利用することができます。
方法は Xcode11 で起動しているシミュレータは終了した上で、Xcode12 でインストールしてあるシミュレータを起動して、Xcode11.x でアプリをビルドするだけでした。

これで Xcode12 対応されるまで凌げそうです🍵

iOS でアップデートアラートを出せる Siren に PR 送ったけどダメだった

最近 Siren というライブラリに PR を送ったので、今回はそれについて書こうと思います。

Siren とは

SireniOS アプリでユーザーにアップデートアラートを出したい場合に使えるライブラリです。利用方法についても自分はソースコードを読んで理解する形となってしまったのですが、ドキュメントがあるので特に困ることはないライブラリだと思います。

実際にどのようなアラートを出せるかというと ↓ の画像のように3種類のアラートを出せるようなライブラリになっています。

f:id:kalupas:20201004122716p:plain

画像から分かるとは思いますが、具体的には以下の3種類です(左から)。

  • 「アップデート」の選択肢しか与えないアラート。ユーザーに強制的なアップデートを促したい場合に使用できそう。
  • 「アップデート」と「次回起動時」の選択肢を与えるアラート。後述しますが、自分はこのアラートだと今回想定していた利用シーンでは使えないため、PR を送りました。
  • 「アップデート」と「次回起動時」と「このバージョンはスキップ」の選択肢を与えるアラート。

なぜ PR を送ったのか

今回アップデートアラートを出す際の要件としては主に以下のようなものがありました。

  • アプリのマイナー(メジャー)バージョンが変更された際にアラートを出したい
  • ダウンロードしやすい環境でのみ出すようにしたいため、Wifi 接続時にのみアラートを出したい(こちらは、既存のコードで利用されていることもあり、Reachability.swift というライブラリを利用することにしました)
  • 「ユーザーによっては今はアップデートしたくないという場合もある」かつ「起動時ごとに出すとうるさい」ため、「このバージョンはスキップ」と「アップデート」という選択肢のみを与えるようにしたい

他にも細かい要件はありましたが、重要なのは一番最後になります。前述したように Siren は「このバージョンはスキップ」と「アップデート」の二つの選択肢のみがあるアラートを出すことができません。
自分としても、「アップデート」「次回起動時」の選択肢を与えるアラートだと若干うるさすぎますし、「このバージョンはスキップ」と「アップデート」のみのアラートを出したいということで PR を出しました。

結果は?

出した PR はこちらになります。

結果としてはクローズされてしまうこととなってしまいました。実際自分が出した PR での変更はほんの数行しかないものでした。 -> Files Changed
PR の Conversation を見ていただけるとわかると思いますが、以下のような流れでやり取りが行われました。

  • 自分:「スキップボタン」と「アップデートボタン」だけのアラートが欲しいので PR を出しました!
  • 作者:PR ありがとう!8 年間 Siren は続いてきたけど誰からもその要望はありませんでした!どんな場面で使うのですか?
  • 自分:(今回の要件をサラッと説明した上で)Human Interface Guidelines の Alerts の章にも Alert はできるだけ2つのボタンにしてくださいって書いてあるし、二つのアラートが欲しい!と思って PR を投げました!
  • 作者:私はあなたの考えについて反対ではないです。でも今回追加してくれた機能について私はこう思います。「少数派のユーザー(および or または)開発者にとっての機能であると思う」「Siren の機能を複雑にしてしまう」「三つのボタンを持つアラートは HIG に沿っていませんが、Apple も三つのボタンを持つ AlertController を導入しているので、ある程度 Apple 自身も HIG に沿っているわけではないです
  • 作者:あなたの感情・あなたが一部の人々を気にかけていることを評価しますが、ライブラリに追加することに対して十分な価値があるとは言えないと思いました。特に私は OSS を書く時、少数のユーザーのためよりも大多数のユーザーのための機能を追加する傾向があります。
  • 作者:幸い Siren は最近非常に安定していて、master との同期も頻繁に行う必要はないのでフォークして使ったりしてもらえると嬉しいです!この PR はクローズします、ありがとう!

(自分の英語の拙さは置いておいて)上記の流れのように結果としてはクローズとなりましたが、だいぶ学びがあって良かったと思っています。
普段自分は開発において、少数派のユーザー(および or または)開発者にとっての機能 のために色々あれこれ考えてしまうことが多く、一つ反省すべきだなと感じました(もちろんそれが毎回悪いわけであるとは思わないですが)。
また、HIG に沿うのがベストプラクティスというわけではないことは往々にしてあることなので、作者のおっしゃっていたこともある程度理解することができました。
さらに、OSS に対する考え方?(大多数のユーザーのために機能を追加する〜)のようなものについても学ぶことができて良かったと思っています。
(会話の中で jazzy という Swift のドキュメント自動生成ツールについて知ることができたのもの良かったです。)

おわりに

クローズされてしまい、Siren を利用する以上は fork しない限り二つのボタンのアラートを使用することができない状況となりました。
今回は結論として、fork は運用的にしたくない・デザイナー的にも二つボタンのアラートにしたい という状況から自分でアップデートアラートのための機能を実装することとなりました。

できれば Siren を利用したかったですが、やむなしというところなので実装を頑張っていこうと思います。

SwiftUI の Custom ViewModifier

SwiftUI の ViewModifier は手軽に見た目を変更できるので便利ですね。 最近 SwiftUI を勉強していて、ViewModifier は自分で作成できることも知ったので、備忘録として残しておきます。

ちなみに SwiftUI の勉強には「100 Days of SwiftUI」というコンテンツを利用しています。

www.hackingwithswift.com

このサイトでは、SwiftUI についてはもちろんですが、それ以外にも iOS 周辺の様々な知識を身に付けることができるため、一度やっておいて損はない気がします。

ちなみに今回紹介する ViewModifier についても、このコンテンツの Day23 で詳しく紹介されています。

ViewModifier について

一応、SwiftUI の ViewModifier について軽くだけ説明を挟んでおきます。
基本的に名前の通りで、View に付けることができる修飾子であり、一つの View の見た目などを手軽に変更することができるものになります。

例えば、↓ のようなコードを書くと、

struct ContentView: View {
    var body: some View {
        Color(.red)
    }
}

↓ のような一面真っ青な画面が出来上がります。

f:id:kalupas:20200928234505p:plain:w200

これは、↓ のように .frame(width:height: ) という ViewModifier を付けてあげるだけで、青い部分のサイズを変更することができるようになります。

struct ContentView: View {
    var body: some View {
        Color(.systemBlue)
            .frame(width: 100, height: 100)
    }
}

このコードでは ↓ のように表示されるようになります。

f:id:kalupas:20200928235723p:plain:w200

非常に簡単にですが、これが基本的な ViewModifier になります。ちなみに ↑ のコードで Color を View として扱えているのは、SwiftUI においては Color は View だからになります。 完全に余談ではありますが、Color を覗いていくと ↓ のように View を継承して定義されています。

extension Color : View {

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required `body` property.
    public typealias Body = Never
}

Custom ViewModifier

最初に述べたように SwiftUI の VIewModifier は自分で作成することができます。ViewModifier は ViewModifier プロトコルに適合させることで作成することができます。

ViewModifier プロトコルを見ます。

public protocol ViewModifier {

    /// The type of view representing the body.
    associatedtype Body : View

    /// Gets the current body of the caller.
    ///
    /// `content` is a proxy for the view that will have the modifier
    /// represented by `Self` applied to it.
    func body(content: Self.Content) -> Self.Body

    /// The content view type passed to `body()`.
    typealias Content
}

↑ のプロトコルを見るに、基本的には func body(content: ) -> Self.Body を実装すれば作成することができそうです。

一つ具体例を示します。今回は Text に対して .font(.largeTitle) , .foregroundColor(.white) , .background(Color.red) を付与できるような Modifier を作成します。コードで示すと ↓ のようになります。

struct Title: ViewModifier {
    func body(content: Content) -> some View {
        content
            .font(.largeTitle)
            .foregroundColor(.white)
            .background(Color.red)
    }
}

↑ で Title という名前で Modifier を定義したので、↓ のようにこの Modifier を利用することができます。

struct ContentView: View {
    var body: some View {
        Text("This is Text")
            .modifier(Title())
    }
}

すると、↓ のように自作した Modifier が適用されていることを確認できます。

f:id:kalupas:20200929002908p:plain:w200

しかし、自作した Modifier をいちいち .modifire(ModifierName()) のような形で利用するのは微妙ですよね。

これを解決するには View の extension 内で以下のように宣言するようにすると良いです。

extension View {
    func largeTitle() -> some View {
        self.modifier(Title())
    }
}

↑ のように宣言することによって、Custom Modifier を以下のような形で普通の Modfiier のように扱うことができるようになります。

struct ContentView: View {
    var body: some View {
        Text("This is Text")
            .largeTitle()
    }
}

定義方法を変えただけなので、見た目は特に変わらないです。

また、ViewModifier は Modifier だけではなく View 構造を作ることもできます。例えば、そのような ViewModifier は ↓ のように定義することができます。

struct WhiteOnTextLabel: ViewModifier {
    var text: String
    
    func body(content: Content) -> some View {
        ZStack(alignment: .bottomTrailing) {
            content
            Text(text)
                .font(.caption)
                .foregroundColor(.white)
                .padding(5)
                .background(Color.white)
        }
    }
}

extension View {
    func whiteOnTextLabel(with text: String) -> some View {
        self.modifier(WhiteOnTextLabel(text: text))
    }
}

↑ は ↓ のように使用することができます。

struct ContentView: View {
    var body: some View {
        Color.black
            .frame(width: 300, height: 300)
            .whiteOnTextLabel(with: "White text label")
    }
}

f:id:kalupas:20200929222211p:plain:w200

定義した whiteOnTextLabel(with: ) を利用するだけで、Color View の上に Text を載せるような Custom ViewModifier を作成することができました。

Custom ViewModifier は自分で ViewModifier を作成して簡単に利用することができますし、複数箇所でも利用することができます。UI が十分に共通化されてさえいれば、コードの無駄もかなり減らせるのではないでしょうか。しかし、チーム開発などで使いすぎるとかえって可読性を損なわせてしまったりもしそうなので、使いどころはしっかり考えていきたいですね。

UITableViewCellのprepareForReuseとNSLayoutConstraintのpriority

はじめに

iOSエンジニアとして4月から働きはじめた新卒です。
今後iOSエンジニアとしてやっていくに当たって、ちょっとしたアウトプットの場を作りたいなと思い、ブログに自分が困ったことや、今後忘れたくないことを備忘録的なものとして残していこうと思います。
Flutterもやってたり、バックエンドにも興味はあるので、その内色々アウトプットしていこうと思います。

今日はとりあえずiOS開発周りで最近実装した部分について書いていきます。

UITableViewCellのprepareForReuse

まずはUITableViewCellのprepareForReuseについてです。
大学の時にSwiftをやったことは少しだけあって、その時はよくあるリスト形式のお知らせが流れてくるようなアプリを作っていました。
そのため、UITableViewを使用したこともあったし、Cell周りも実装したりしていたので、prepareForReuseを使ったことはありました。
ただ、最近prepareForReuseを改めて使用するような場面になって、結果的にはprepareForReuseをその際に使用してはいけないということに気づくことができました。

そのことについて詳しく説明する前に、前提としてUITableViewのCellについて少しだけ書いていこうと思います。
基本的にTableViewでCellの内容を決めていく際には、以下のようなtableView(_:cellForRowAt: )を使用することになると思います。

override func tableView(_ tableView: UITableView, 
             cellForRowAt indexPath: IndexPath) -> UITableViewCell {
   // dequeueReusableCell -> セルを再利用
   let cell = tableView.dequeueReusableCell(withIdentifier: "basicStyle", for: indexPath)

   cell.textLabel!.text = "Title text"
   cell.imageView!.image = UIImage(named: "kalupas")
   return cell
}

Apple Developer Documentationの文章を抜粋します。

When you need a cell object at runtime, call the table view’s dequeueReusableCell(withIdentifier:for:) method, passing the reuse identifier for the cell you want. The table view maintains an internal queue of already-created cells. If the queue contains a cell of the requested type, the table view returns that cell. If not, it creates a new cell using the prototype cell in your storyboard. Reusing cells improves performance by minimizing memory allocations during critical times, such as during scrolling.

公式ドキュメントにも書かれている通りで、基本的にTableViewのCellは同じようなものが連続しているので、上記のコードのdequeueReusableCellでセルを生成することによって、再利用できるようになっています。
そして、再利用されることによって、パフォーマンスも良くなると書かれています(例えばスクロール時など)。
ただ、再利用できるばかりに自分のような初心者は、よくこの再利用Cellの罠にハマってしまいがちな気がしています。 具体的には、別々の画像が載せられたCellを実装したはずなのに、Cellが再利用されることによって、Cell内の画像までもが再利用されてしまい、同じ画像が載せられたCellがTableViewに並んでしまうような現象があります。
このような現象を回避するために、大学の時の自分はprepareForReuseを使用していました。
prepareForReuseが何なのかは、公式ドキュメントを参照します。

If a UITableViewCell object is reusable—that is, it has a reuse identifier—this method is invoked just before the object is returned from the UITableView method dequeueReusableCell(withIdentifier:). For performance reasons, you should only reset attributes of the cell that are not related to content, for example, alpha, editing, and selection state. The table view's delegate in tableView(_:cellForRowAt:) should always reset all content when reusing a cell. If the cell object does not have an associated reuse identifier, this method is not called. If you override this method, you must be sure to invoke the superclass implementation.

prepareForReuseは基本的に、Cellを再利用しようとするその直前に呼ばれます。
つまり、prepareForReuseの中で、例えば画像をnilにしてあげたりすることによって、再利用前にCell内の画像が初期化され、Cell内には異なる画像が並ぶようになります。
しかし、これはprepareForReuseの(多分よくある)誤った利用方法になります。
上記公式ドキュメントにしっかりと書かれているのですが、prepareForReuseはパフォーマンス的な観点で、Cellの内容に関係のないもの(例えば、alpha・editing・selectionなど)の状態をリセットする時にしか使うべきではないとされています。
では、Cellごとに画像は再利用されて欲しくないなどの場合はどのように実装を行えば良いのでしょうか。
それはCellの内容を設定する時に利用しているtableView(_:cellForRowAt: )メソッドになります。
という基本的なことが理解できていなかったので、もし同じような問題にハマった人がいればtableView(_:cellForRowAt: )メソッドの中でCellの内容を設定するようにしてみると良いかと思います。

NSLayoutConstraintのpriority

こちらも、またView周りのことでハマったことです。
こちらのpriorityは例えば、AutoLayoutなどでViewに制約をつける時に利用します。普通に一つずつ制約を付けていく場合に利用することはないと思うのですが、場合によってViewの右側の間隔を32pxではなく、48pxにしたい...みたいな時に利用します。
ちょっとわかりにくいかもですが、イメージは↓みたいな感じです。

Cell double constraint image
Cellの同じ部分に二つ制約が付いた状態

では、場合によって、この二つの制約のどちらかを有効にしたい。そんな場合はどうすれば良いでしょうか?
答えは簡単で、制約にはpriority(優先度)というプロパティが存在しています。
そのpriority@property UILayoutPriority priority;という形で定義されています。
では、priorityの型であるUILayoutPriorityは何なのかというと、typedef float UILayoutPriority;のように定義されています。typedefObjective-Cの記法なので、あまり気にする必要はありませんが名前の通り型の定義みたいな感じです。
その定義の中では色々プロパティが定義されていて、UILayoutPriorityRequiredとかUILayoutPriorityDefaultHighUILayoutPriorityDefaultLowなどがあります。
これらそれぞれについて詳しく説明すると既に長いのに、さらに長くなってしまいそうな気がしているので、公式ドキュメントを参照していただければと思います。
では、戻って、このUILayoutPriority priorityの値の変更方法についてですが、floatなので数値で指定してあげることもできますし、独自にプロパティとして定義されている前述したUILayoutPriorityRequired等を指定して変更することができます。
しかし、値を変更する際に注意すべきポイントがあります。まずは例のごとく公式ドキュメントを参照します。

Priorities may not change from nonrequired to required, or from required to nonrequired. An exception will be thrown if a priority of NSLayoutPriorityRequired in macOS or UILayoutPriorityRequired in iOS is changed to a lower priority, or if a lower priority is changed to a required priority after the constraints is added to a view. Changing from one optional priority to another optional priority is allowed even after the constraint is installed on a view.

注意すべきポイントとは、このpriorityの値はnonrequiredからrequiredにすることもできないし、requiredからnonrequiredにすることもできないよというものです。
自分は、例で言うと32pxではなく48pxにしたい...みたいな時に、
変更前:32pxのpriority -> required, 48pxのpriority -> nonrequired
変更後:32pxのpriority -> nonrequired, 48pxのpriority -> required
のように変更することによって、動的な制約の変更を実現しようとしました。しかし無理でした。
原因は上に説明した通りです。なので、このように動的に制約を変更したいという場合は、最初から二つとものprioritynonrequiredな値(1000未満)に設定しておいて、その後で優先度を高くしたい制約のpriorityrequired(1000)以外の値で、もう一つの制約より高い数値にしてあげれば良いかと思います。

おわりに

以上View周りで最近困ったことについてアウトプットしてみました。
説明がわかりにくい上に、非常に長い文章になってしまったので、アウトプットしつつ文章の質も高めていくことができればと思います。
ちょっと疲れましたが、また次回以降も書けるように頑張っていきたいです。