技術本部 Eight Engineering Unit の Mobile Applicationグループで、25卒内定者インターンをしている松山(@akidon0000)です。
今回、Eightの名刺表示画面における、枠線のデザイン修正を担当しました。しかし、実装の過程で枠線が表示されない不具合が発生しました。当初は名刺画像の非同期表示によって枠線が上書きされていると考え、調査を進めたものの、実際には別の原因があることが判明しました。
本記事では、問題の詳細とEightでの解決方法、そしてそこから得られた学びをご紹介します。
今回の記事の目次は次の通りです。
担当タスクで生じた課題と想定した原因
Eightでは、白地の名刺とアプリの背景色との境界を認識しやすくなるよう、図1の様にUIImage
へ枠線を描画する処理を実装しています。
まずは、未実装の枠線生成タスクに取り組みました。
当初、画像に枠線をつける処理をXIBファイルのオブジェクトを初期化する際に呼ばれるinit?(coder: NSCoder)
内に記述していましたが、枠線は表示されませんでした。
public final class EightCardImageView: UIImageView { private let cardImageBorderWidth: CGFloat = 1.0 private let cardImageBorderColor: UIColor = EightColor.veryLightGray public required init?(coder: NSCoder) { super.init(coder: coder) applyBorder() // ここで枠線処理を呼び出し } private func applyBorder() { layer.borderWidth = cardImageBorderWidth layer.borderColor = cardImageBorderColor } }
ここで、私はinit?(coder: NSCoder)
で枠線描画後に名刺画像がセットされるので、枠線が画像によって上書きされているのではと考え、検証を進めました。
真の原因
サンプルプロジェクトを作成して動作確認をしていたところ、図2の様にinit?(coder: NSCoder)
でも画像表示のタイミングに関係なく描画されていることが判明しました。また、画像をセットした後も図3の様に上書きされることなく枠線は表示されています。
そこから、Eightのソースコード内で設定が上書きされているのではと考え、さらに調査を進めました。
調査の結果、枠線が表示されない原因は、図4と図5に示すXcodeのInterface Builderでカスタムプロパティを設定できるUser Defined Runtime Attributes
において、UIImageView
のlayer.borderWidth
が0に設定され、上書きされていたためと判明しました。
XIBにおけるUI描画の適切な場所
結論として、仕様にもよりますがUIの描画処理はUser Defined Runtime Attributes
または awakeFromNib
に記述するのが適していると分かりました。
init?(coder: NSCoder)
は、XIBやStoryboardからオブジェクトを復元するために使用されるメソッドです。Xcodeのビルドプロセスでは、XIBファイル(XML形式)がNIB(NeXT Interface Builder)形式に変換され、実行時にはこのNIBファイルからオブジェクトが生成されます。このプロセスでNSCoder
を使用してデータをデコードし、その内容を基にUI要素を初期化します。*1
具体的には、XIBの読み込み時に次の順序で各処理と設定が実行・反映されます。
1, init?(coder: NSCoder)の呼び出し
ここでは、NSCoderからデコードされた情報を基にオブジェクトの基本的なプロパティやレイアウトを初期化します。NSCodingプロトコルに従って、init?(coder:)は、NSCoderから渡されたデータを使用してオブジェクトのインスタンス変数を復元します。*2
また、
Because the order in which objects are instantiated from an archive is not guaranteed, your initialization methods should not send messages to other objects in the hierarchy.
awakeFromNib() | Apple Developer Documentation
アーカイブからオブジェクトがインスタンス化される順序は保証されていないため、初期化メソッドでは階層内の他のオブジェクトにメッセージを送信しないようにすべきです。
これより、初期化メソッド内ではUIに関する記述は避けるべきということがわかります。
2, User Defined Runtime Attributesの反映
次に、XcodeのInterface Builder(IB)で設定されたカスタムプロパティが適用されます。ここでは、ボーダーの幅、色、角丸の半径など、XIBやStoryboard上で設定されたカスタムプロパティが反映されます。今回の不具合の原因となったのは、このステップでborderWidthが0に設定されていたため、initでの設定が上書きされ、枠線が表示されませんでした。
IBで記述するメリットとしては、コードを書かずにUI要素のプロパティを直感的に設定できるため、実装者に依存せず一貫した記述を行える点が挙げられます。
3, awakeFromNibの呼び出し
The nib-loading infrastructure sends an awakeFromNib message to each object recreated from a nib archive, but only after all the objects in the archive have been loaded and initialized. When an object receives an awakeFromNib message, it is guaranteed to have all its outlet and action connections already established.
awakeFromNib() | Apple Developer Documentation
Nib読み込みインフラストラクチャは、Nibアーカイブから再作成された各オブジェクトに対してawakeFromNibメッセージを送信しますが、これはアーカイブ内のすべてのオブジェクトが読み込まれ、初期化された後にのみ行われます。オブジェクトが awakeFromNib メッセージを受信したとき、そのオブジェクトのアウトレットとアクションの接続はすべて確立済みであることが保証されます。
これより、awakeFromNib
はすべてのプロパティ設定が完了した後に呼ばれるメソッドであることが分かります。すべてのインターフェイス要素が正しく読み込まれた状態で、UI関連の処理を行えるため、動的なカスタマイズや複雑なレイアウト調整が可能です。
今回のタスクにおけるベストプラクティス
今回のタスクでは、UIImageView
に画像がセットされたタイミングで枠線を描画する実装にしました。名刺画像が表示されていない時は枠線を描画しないことで、ユーザーに混乱を招くことを防ぎ、ユーザー体験の向上につながると判断したためです。
そのため、User Defined Runtime Attributes
やawakeFromNib
ではなくdidSet
を使用して実装し、問題を解決しています。
public final class EightCardImageView: UIImageView { private let cardImageBorderWidth: CGFloat = 1.0 private let cardImageBorderColor: UIColor = EightColor.veryLightGray override public var image: UIImage? { didSet { applyBorder() // ここで枠線処理を呼び出し } } private func applyBorder() { let hasImage = (image != nil) layer.borderWidth = hasImage ? cardImageBorderWidth : 0 layer.borderColor = hasImage ? cardImageBorderColor.cgColor : UIColor.clear.cgColor } }
今回の担当タスクから得た学び
学んだことは2つあります。
1つ目は、"自身のUIKit理解不足"です。
XIBファイルの仕組みやインスタンス生成時に呼ばれる順番を理解していれば、init
で反映できない問題に直面した際にUser Defined Runtime Attributes
を確認して解決できており、非常に大きな学びとなりました。
また今回は、画像の非同期表示による描画問題だと思いこみ、User Defined Runtime Attributes
を確認しなかったことが問題だったと思っています。これに関しては、知見のあるチームメンバーとペアプロを行うことが有効であると感じ、トラブル時の解決手法を学ぶ良い機会となりました。
2つ目は、"チーム開発する上での方針"です。
モバイルアプリ開発では、Interface Builderとプログラムコードのどちらを主体的に使ってUIを構築していくかは、それぞれの利点がありプロジェクトの状況によるという知見を得ました。普段は個人で開発することが多いため、チーム開発における視点を学ぶ貴重な機会となりました。
今回の経験を通して、課題解決における新たなアプローチや知識を学びました。これを基に、今後も新たなチャレンジに積極的に取り組み、成長し続けたいと考えています。