はじめに (対象読者・この記事でわかること)
この記事は、iOSアプリ開発に携わるSwiftエンジニアの方、特にSwiftUIまたはUIKitを用いてカスタムUI要素を実装したいと考えている方を対象としています。既存のViewの標準機能では物足りず、特定のデザイン要件に合わせてViewの特定の辺にのみ、異なる色や太さの線(ボーダー)を柔軟に描画する方法を探している方に最適です。
この記事を読むことで、SwiftUIではViewModifierとoverlayを、UIKitではCALayerを活用し、Viewの上下左右の各辺に個別のスタイル(色や太さ)を持つ線を描画する具体的な実装方法がわかります。また、UIKitにおけるCALayer利用時のレイアウトに関する注意点とその解決策も習得でき、より洗練されたカスタムUIを構築できるようになるでしょう。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- Swiftの基本的な文法とプログラミングの概念
- SwiftUIまたはUIKitを用いたiOSアプリ開発の基本的な経験
- Viewのレイアウト(Auto LayoutやStack/VStackなど)に関する基礎知識
- (UIKitの場合) UIViewとCALayerの基本的な関係性
Viewの各辺に個別の線が必要な理由
iOSアプリ開発において、Viewにボーダー(線)を設定することは頻繁にあります。しかし、標準的なUIフレームワークが提供するボーダー機能には限界があります。例えば、SwiftUIのborder()修飾子やUIKitのlayer.borderWidthは、Viewの四辺全てに均一な色と太さのボーダーを適用します。
しかし、実際のUIデザインでは、以下のような特定の要件が出てくることがあります。
- 視覚的な区切り: リストの各セル間で、下辺のみに細い区切り線を引きたい。
- 要素の強調: 特定の入力フィールドや選択されたアイテムの枠線で、下辺だけを太くしたり、異なる色にしたりして強調したい。
- デザインの一貫性: ブランドガイドラインで、特定のコンポーネントには上辺と左辺のみに特定のラインを引くことが定められている。
これらのケースでは、標準機能では対応しきれません。各辺に個別の設定ができるカスタムボーダーの実装が必要となります。本記事では、この課題を解決するための具体的なアプローチを、人気の二つのフレームワーク(SwiftUIとUIKit)それぞれで詳しく解説します。
具体的な実装方法: 各辺に異なる線を引く
ここでは、SwiftUIとUIKitそれぞれで、Viewの各辺に異なる線を描画する方法を具体的なコード例を交えて解説します。
SwiftUIでの実装:ViewModifierとoverlayを活用する
SwiftUIでは、overlay修飾子とRectangleシェイプを組み合わせることで、Viewの任意の辺に線を描画できます。さらに、これをViewModifierとしてカプセル化することで、再利用性の高いカスタムボーダーを簡単に実現できます。
ステップ1:overlayを使った基本的な線の描画
まずは、overlay修飾子を使ってViewの上下左右に異なる線を描画する基本的な方法を見ていきましょう。
Swiftimport SwiftUI struct CustomBorderView: View { var body: some View { Text("Hello, Custom Border!") .font(.title2) .padding() .frame(width: 250, height: 120) .background(Color.white) .cornerRadius(10) .shadow(radius: 5, x: 0, y: 5) .overlay(alignment: .top) { // 上辺に赤色の線 Rectangle() .frame(height: 2) .foregroundColor(.red) } .overlay(alignment: .bottom) { // 下辺に青色の線 Rectangle() .frame(height: 4) .foregroundColor(.blue) } .overlay(alignment: .leading) { // 左辺に緑色の線 Rectangle() .frame(width: 1) .foregroundColor(.green) } .overlay(alignment: .trailing) { // 右辺にオレンジ色の線 Rectangle() .frame(width: 3) .foregroundColor(.orange) } } } #Preview { CustomBorderView() }
このコードでは、Textビューにoverlay修飾子を複数適用しています。
- overlay(alignment: .top): ビューの上端に線を描画します。Rectangle()のframe(height: ...)で線の太さを指定し、foregroundColorで色を設定しています。
- 同様に、.bottom, .leading, .trailingのalignmentを指定することで、それぞれの辺に個別の線を描画しています。
ステップ2:より柔軟なViewModifierの作成
上記の例はシンプルですが、毎回overlayを記述するのは冗長です。そこで、各辺の色や太さを引数で受け取れるViewModifierを作成し、再利用性を高めましょう。
Swiftimport SwiftUI /// Viewの特定の辺にカスタムボーダーを追加するViewModifier struct EdgeBorder: ViewModifier { var edge: Edge.Set // ボーダーを追加する辺 (例: .top, .bottom) var color: Color // ボーダーの色 var thickness: CGFloat // ボーダーの太さ func body(content: Content) -> some View { content .overlay(alignment: alignment(for: edge)) { // 指定された辺のAlignmentでオーバーレイ Rectangle() .fill(color) // 指定色で塗りつぶし // 辺の向きに応じてframeのwidthまたはheightをthicknessに設定 .frame(width: isHorizontal(edge) ? nil : thickness, height: isHorizontal(edge) ? thickness : nil) } } /// `Edge.Set`から対応する`Alignment`を返すヘルパーメソッド private func alignment(for edge: Edge.Set) -> Alignment { if edge.contains(.top) { return .top } if edge.contains(.bottom) { return .bottom } if edge.contains(.leading) { return .leading } if edge.contains(.trailing) { return .trailing } return .center // どの辺も指定されない場合のデフォルト } /// 指定された辺が水平方向(上辺または下辺)かどうかを判定するヘルパーメソッド private func isHorizontal(_ edge: Edge.Set) -> Bool { edge.contains(.top) || edge.contains(.bottom) } } extension View { /// Viewの特定の辺にカスタムボーダーを適用するための拡張メソッド /// - Parameters: /// - edge: ボーダーを追加する辺 (.top, .bottom, .leading, .trailing を組み合わせることも可能) /// - color: ボーダーの色 /// - thickness: ボーダーの太さ /// - Returns: カスタムボーダーが適用されたView func border(_ edge: Edge.Set, color: Color, thickness: CGFloat) -> some View { self.modifier(EdgeBorder(edge: edge, color: color, thickness: thickness)) } } struct CustomBorderModifierExample: View { var body: some View { VStack(spacing: 30) { Text("ViewModifier Applied") .font(.headline) .padding() .frame(width: 280, height: 150) .background(Color.white) .cornerRadius(15) .shadow(radius: 8, x: 0, y: 8) // 拡張メソッドを使ってカスタムボーダーを適用 .border(.top, color: .purple, thickness: 3) .border(.bottom, color: .pink, thickness: 5) .border(.leading, color: .cyan, thickness: 1) .border(.trailing, color: .indigo, thickness: 2) Text("Combined Edges Example") .font(.headline) .padding() .frame(width: 280, height: 80) .background(Color.yellow.opacity(0.2)) .cornerRadius(10) .border([.top, .bottom], color: .brown, thickness: 2) // 上下両方に同じ線 .border(.leading, color: .red, thickness: 4) // 左辺に別の線 } .padding() .background(Color.gray.opacity(0.1)) .edgesIgnoringSafeArea(.all) } } #Preview { CustomBorderModifierExample() }
このViewModifierとViewの拡張により、任意のViewに対してチェーン形式でborder(_:color:thickness:)を呼び出すだけで、柔軟なカスタムボーダーを設定できるようになります。
UIKitでの実装:CALayerを活用する
UIKitでは、CALayerというCore Animationフレームワークのオブジェクトを利用して、Viewの描画を細かく制御できます。Viewのボーダーは、UIViewのlayerプロパティ(CALayerのサブクラスであるCALayerインスタンス)を通じてアクセスできますが、各辺に異なる設定をするには、個別のCALayerインスタンスをサブレイヤーとして追加する必要があります。
ステップ1:CALayerを使った基本的な線の描画
UIViewの拡張メソッドを作成し、指定された辺にCALayerを追加して線を表現します。
Swiftimport UIKit extension UIView { /// ビューの指定された辺に線を追加します。 /// このメソッドは、ビューのフレームが確定した後(例: `viewDidLayoutSubviews`内や`DispatchQueue.main.async`後)に呼び出す必要があります。 /// - Parameters: /// - edge: 線を追加する辺 (例: .top, .bottom, .left, .right)。 /// - color: 線の色。 /// - thickness: 線の太さ。 func addBorder(to edge: UIRectEdge, color: UIColor, thickness: CGFloat, identifier: String = "customBorder") { // 既存の同じ識別子のボーダーレイヤーを削除して重複を防ぐ layer.sublayers?.filter { $0.name == identifier && $0.value(forKey: "edge") as? UIRectEdge == edge }.forEach { $0.removeFromSuperlayer() } let border = CALayer() border.name = identifier // 識別子を設定 border.backgroundColor = color.cgColor border.setValue(edge, forKey: "edge") // どの辺に属するかを保持(削除時の特定用) switch edge { case .top: border.frame = CGRect(x: 0, y: 0, width: bounds.width, height: thickness) case .bottom: border.frame = CGRect(x: 0, y: bounds.height - thickness, width: bounds.width, height: thickness) case .left: border.frame = CGRect(x: 0, y: 0, width: thickness, height: bounds.height) case .right: border.frame = CGRect(x: bounds.width - thickness, y: 0, width: thickness, height: bounds.height) default: return // UIRectEdge.all などはここでは対応しない } layer.addSublayer(border) } /// ビューの四辺全てに異なる線を追加します。 /// このメソッドは、ビューのフレームが確定した後(例: `viewDidLayoutSubviews`内や`DispatchQueue.main.async`後)に呼び出す必要があります。 /// - Parameters: /// - top: 上辺の線の色と太さ。nilの場合は線を描画しない。 /// - bottom: 下辺の線の色と太さ。nilの場合は線を描画しない。 /// - left: 左辺の線の色と太さ。nilの場合は線を描画しない。 /// - right: 右辺の線の色と太さ。nilの場合は線を描画しない。 func addCustomBorders(top: (color: UIColor, thickness: CGFloat)? = nil, bottom: (color: UIColor, thickness: CGFloat)? = nil, left: (color: UIColor, thickness: CGFloat)? = nil, right: (color: UIColor, thickness: CGFloat)? = nil, identifier: String = "customBorder") { // 既存のカスタムボーダーレイヤーを全て削除 layer.sublayers?.filter { $0.name == identifier }.forEach { $0.removeFromSuperlayer() } if let t = top { addBorder(to: .top, color: t.color, thickness: t.thickness, identifier: identifier) } if let b = bottom { addBorder(to: .bottom, color: b.color, thickness: b.thickness, identifier: identifier) } if let l = left { addBorder(to: .left, color: l.color, thickness: l.thickness, identifier: identifier) } if let r = right { addBorder(to: .right, color: r.color, thickness: r.thickness, identifier: identifier) } } /// 特定の識別子を持つカスタムボーダーをすべて削除します。 func removeCustomBorders(identifier: String = "customBorder") { layer.sublayers?.filter { $0.name == identifier }.forEach { $0.removeFromSuperlayer() } } } // 実際に使用するViewControllerの例 class UIKitCustomBorderViewController: UIViewController { private let customView: UIView = { let view = UIView() view.backgroundColor = .systemBackground view.layer.cornerRadius = 10 view.layer.shadowColor = UIColor.black.cgColor view.layer.shadowOpacity = 0.2 view.layer.shadowOffset = CGSize(width: 0, height: 5) view.layer.shadowRadius = 5 view.translatesAutoresizingMaskIntoConstraints = false return view }() private let label: UILabel = { let label = UILabel() label.text = "Hello, UIKit Custom Border!" label.textColor = .label label.textAlignment = .center label.font = .preferredFont(forTextStyle: .headline) label.translatesAutoresizingMaskIntoConstraints = false return label }() override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemGray6 view.addSubview(customView) customView.addSubview(label) NSLayoutConstraint.activate([ customView.centerXAnchor.constraint(equalTo: view.centerXAnchor), customView.centerYAnchor.constraint(equalTo: view.centerYAnchor), customView.widthAnchor.constraint(equalToConstant: 280), customView.heightAnchor.constraint(equalToConstant: 150), label.centerXAnchor.constraint(equalTo: customView.centerXAnchor), label.centerYAnchor.constraint(equalTo: customView.centerYAnchor) ]) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() // customViewのフレームが確定した後にボーダーを追加 // フレーム変更に対応するため、ここで毎回ボーダーを更新する(既存を削除して追加し直し) customView.addCustomBorders( top: (color: .red, thickness: 3), bottom: (color: .blue, thickness: 5), left: (color: .green, thickness: 1), right: (color: .orange, thickness: 2) ) } } // プレビュー用のコード(Xcode 15以降で利用可能) import SwiftUI struct UIKitCustomBorderViewController_Previews: PreviewProvider { static var previews: some View { UIViewControllerPreview { UIKitCustomBorderViewController() } .edgesIgnoringSafeArea(.all) } } // UIViewControllerをSwiftUIプレビューで表示するためのヘルパー struct UIViewControllerPreview<ViewController: UIViewController>: UIViewControllerRepresentable { let viewControllerBuilder: () -> ViewController init(_ viewControllerBuilder: @escaping () -> ViewController) { self.viewControllerBuilder = viewControllerBuilder } func makeUIViewController(context: Context) -> ViewController { viewControllerBuilder() } func updateUIViewController(_ uiViewController: ViewController, context: Context) { // 必要であればここでUIを更新 } }
このコードでは、UIViewの拡張としてaddBorderとaddCustomBordersメソッドを定義しています。
- addBorderは、指定されたUIRectEdgeに応じてCALayerのframeを設定し、Viewのlayerにサブレイヤーとして追加します。
- addCustomBordersは、複数の辺に対してaddBorderをまとめて呼び出す便利なメソッドです。
ハマった点やエラー解決
UIKitでCALayerを扱う際に最もハマりやすい点の一つが、CALayerのframeがAuto Layoutの影響を受けないことです。
問題点:
1. 描画タイミング: viewDidLoad()の時点でViewのframe(サイズ)はまだ確定していません。このタイミングでCALayerのframeを設定してしまうと、正しいサイズで描画されません。
2. レイアウト変更への追従: デバイスの回転やViewのサイズ変更(Auto Layoutによる更新)が発生しても、CALayerのframeは自動的に更新されません。そのため、ボーダーがViewのサイズ変更に追従せず、不自然な表示になることがあります。
3. 重複描画: addBorderやaddCustomBordersを複数回呼び出すと、同じ場所にボーダーが重複して追加されてしまい、意図しない描画になることがあります。
解決策
- 描画タイミングの調整:
CALayerのframeを設定する処理は、Viewのサイズが確定した後に行う必要があります。これには以下のタイミングが適しています。viewDidLayoutSubviews(): ViewコントローラーのView階層がレイアウトを完了した後、毎回呼び出されます。DispatchQueue.main.async: Viewが画面に表示され、レイアウトが完了した後に処理を実行します。
- レイアウト変更への追従:
viewDidLayoutSubviews()内でボーダーの追加処理を呼び出すようにします。この際、前述の重複描画を避けるために、既存のカスタムボーダーレイヤーを一度削除してから再追加するロジックをaddCustomBordersメソッド内に実装しています。これにより、Viewのサイズが変更されるたびにボーダーが正しく再描画されます。CALayerのnameプロパティを使って、カスタムボーダーとして追加したレイヤーを識別し、削除対象とすることで、他のシステムレイヤー(例:shadowLayerなど)に影響を与えないようにしています。
これらの解決策を講じることで、UIKitでも堅牢なカスタムボーダーを実装することが可能です。
まとめ
本記事では、SwiftUIとUIKitの両方で、Viewの各辺に異なる色や太さの線(ボーダー)を柔軟に描画する方法 を解説しました。
- SwiftUI:
overlay修飾子とViewModifierを組み合わせることで、再利用性の高いカスタムボーダー拡張を実装できます。これにより、宣言的な構文で直感的に各辺のボーダーを制御できます。 - UIKit:
CALayerをViewのサブレイヤーとして追加し、それぞれのCALayerのframeとbackgroundColorを個別に設定することで、辺ごとのカスタムボーダーを実現できます。
この記事を通して、読者は標準機能の限界を超えて、デザイン要件に合わせた高度なカスタムボーダーを自身のiOSアプリケーションに実装できるようになるでしょう。 特にUIKitにおけるCALayerの描画タイミングとレイアウト更新の課題に対する理解を深めることで、より堅牢なUIコンポーネティを開発するスキルが身についたはずです。
今後は、線の角丸処理、破線(点線)の実装、線のグラデーション、あるいはアニメーションを伴うボーダーなど、発展的な内容についても記事にする予定です。
参考資料
- Apple Developer Documentation: ViewModifier
- Apple Developer Documentation: CALayer
- Apple Developer Documentation: UIRectEdge
- Hacking with Swift: How to add a custom border to a view
