Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

名刺の枠線が表示されない?〜枠線消失の原因について〜

技術本部 Eight Engineering Unit の Mobile Applicationグループで、25卒内定者インターンをしている松山(@akidon0000)です。

今回、Eightの名刺表示画面における、枠線のデザイン修正を担当しました。しかし、実装の過程で枠線が表示されない不具合が発生しました。当初は名刺画像の非同期表示によって枠線が上書きされていると考え、調査を進めたものの、実際には別の原因があることが判明しました。
本記事では、問題の詳細とEightでの解決方法、そしてそこから得られた学びをご紹介します。
今回の記事の目次は次の通りです。

担当タスクで生じた課題と想定した原因

Eightでは、白地の名刺とアプリの背景色との境界を認識しやすくなるよう、図1の様にUIImageへ枠線を描画する処理を実装しています。

図1:枠線を表示させた名刺画像

まずは、未実装の枠線生成タスクに取り組みました。

当初、画像に枠線をつける処理を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の様に上書きされることなく枠線は表示されています。

図2:UIImageViewに画像をセットする前
図3:画像をセットした後

そこから、Eightのソースコード内で設定が上書きされているのではと考え、さらに調査を進めました。

調査の結果、枠線が表示されない原因は、図4と図5に示すXcodeのInterface Builderでカスタムプロパティを設定できるUser Defined Runtime Attributesにおいて、UIImageViewlayer.borderWidthが0に設定され、上書きされていたためと判明しました。

図4:名刺画像を表示しているXIB
図5:User Defined Runtime Attributes

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.
Nib読み込みインフラストラクチャは、Nibアーカイブから再作成された各オブジェクトに対してawakeFromNibメッセージを送信しますが、これはアーカイブ内のすべてのオブジェクトが読み込まれ、初期化された後にのみ行われます。オブジェクトが awakeFromNib メッセージを受信したとき、そのオブジェクトのアウトレットとアクションの接続はすべて確立済みであることが保証されます。

awakeFromNib() | Apple Developer Documentation

これより、awakeFromNibはすべてのプロパティ設定が完了した後に呼ばれるメソッドであることが分かります。すべてのインターフェイス要素が正しく読み込まれた状態で、UI関連の処理を行えるため、動的なカスタマイズや複雑なレイアウト調整が可能です。

今回のタスクにおけるベストプラクティス

今回のタスクでは、UIImageViewに画像がセットされたタイミングで枠線を描画する実装にしました。名刺画像が表示されていない時は枠線を描画しないことで、ユーザーに混乱を招くことを防ぎ、ユーザー体験の向上につながると判断したためです。

そのため、User Defined Runtime AttributesawakeFromNibではなく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を構築していくかは、それぞれの利点がありプロジェクトの状況によるという知見を得ました。普段は個人で開発することが多いため、チーム開発における視点を学ぶ貴重な機会となりました。

今回の経験を通して、課題解決における新たなアプローチや知識を学びました。これを基に、今後も新たなチャレンジに積極的に取り組み、成長し続けたいと考えています。

おわりに

一緒にSansan / Eight のモバイル開発をしていく仲間を募集中です!

採用イベントや新卒採用はこちらから!

jp.corp-sansan.com
jp.corp-sansan.com

© Sansan, Inc.