hachinoBlog

hachinobuのエンジニアライフ

Swift Embedded Framework内でCやObjective-Cのライブラリを使う方法

背景

運用しているSwiftアプリでObjective-C製のCocoaPodsライブラリをSwift Embedded Frameworkに閉じて使いたかった。 アプリケーション側で使うならお決まりのBridging Headerファイルを追加して、そこに適宜書いていけば良いのだけれど、Swift Embedded Frameworkで使いたい場合にどうすれば良いかの備忘録。

やり方

例としてObc製のGoogleAnalyticsライブラリをPods経由でインストールしてSwift Embedded Frameworkで使う方法を紹介する。

まず、TrackerというEmbedded Frameworkが切られているとして、Podfileは下記のようにGoogleAnalyticsを使うTrackerターゲットでに記載する。

# Uncomment the next line to define a global platform for your project
# platform :ios, '9.0'

target 'ObjcLibUseEmbededFramework' do
  # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
  use_frameworks!

  # Pods for ObjcLibUseEmbededFramework

  target 'Tracker' do
    pod 'GoogleAnalytics'
  end

  target 'ObjcLibUseEmbededFrameworkTests' do
    inherit! :search_paths
    # Pods for testing
  end

end

まずは、modulemapファイルを定義する。

プロジェクトファイルの同階層に Libraries/GoogleAnalytics/module.modulemap を作成。

f:id:hachinobu:20190104165737p:plain

中身は下記のようにする。

module GoogleAnalytics {
    // Embedded Frameworkで使いたいヘッダーを定義
    header "../../Pods/GoogleAnalytics/Sources/GAI.h"
    header "../../Pods/GoogleAnalytics/Sources/GAIDictionaryBuilder.h"
    header "../../Pods/GoogleAnalytics/Sources/GAIFields.h"
    link "GoogleAnalytics"
}

まず、module GoogleAnalytics {GoogleAnalytics部分がEmbedded Frameworkでインポートするモジュールになる。

header hogehoge..の部分は先で定義したGoogleAnalyticsモジュールに含める機能のヘッダーを指定する。

この場合、GoogleAnalyticsモジュールはGoogleAnalyticsGAI.h,GAIDictionaryBuilder.h,GAIFields.hを含むことになる。

linkはリンカ処理の時に追加されるモジュールを指定する。

ここに書かない場合は手動でGoogleAnalyticsのStaticLibraryを手動でリンクする必要がある。

手動でやる場合は、

[Build Phases]-[Link Binary With Libraries]に{$SRCROOT}/Pods/GoogleAnalytics/Libraries/libGoogleAnalytics.aを指定する

f:id:hachinobu:20190104170050p:plain

module.modulemapを生成したら次に、Embedded FrameworkのTargetの[Build Settings]-[SWIFT_INCLUDE_PATHS]に、module.modulemapのあるフォルダパス"${$SRCROOT}/Libraries"を指定する。

f:id:hachinobu:20190104170355p:plain

これでEmbedded Framework内で先にmodulemapで定義したモジュール名でインポートできるようになる。

import GoogleAnalytics

f:id:hachinobu:20190104170543p:plain

f:id:hachinobu:20190104170609p:plain

linkerエラーになった場合は、ターゲットの[Build Phases] - [Link Binary With Libraries]に必要なものを追加していく。 GoogleAnalyticsの場合は、自身のStaticLibraryやPodSpecファイルのframeworksやlibrariesを見て追加していく。

https://github.com/CocoaPods/Specs/blob/master/Specs/4/9/c/GoogleAnalytics/3.17.0/GoogleAnalytics.podspec.json#L33

遷移中にNavigationBarの設定をアニメーションさせて綺麗に見せる

背景

PushやModal遷移の時に呼び出し元ViewControllerと呼び出し先ViewControllerでNavigationBarやボタンの色が違う時に、その遷移の進捗に応じて設定の色などをアニメーションで変えて綺麗に見せたいかった

方法

UIViewControllerTransitionCoordinatorのメソッドである

func animate(alongsideTransition animation: ((UIViewControllerTransitionCoordinatorContext) -> Void)?, 
  completion: ((UIViewControllerTransitionCoordinatorContext) -> Void)? = nil) -> Bool

を使えば良い。

UIViewControllerTransitionCoordinatorはUIViewControllerのプロパティとして定義されている。

例えばNavigationBarの設定が違うMainViewControllerとDetailViewControllerあるとする

f:id:hachinobu:20180721212023p:plainf:id:hachinobu:20180721212034p:plain
遷移元MainViewControllerと遷移先DetailViewController

設定は呼び出し元のMainViewController.swifが

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        self.navigationController?.navigationBar.barTintColor = .blue
        self.navigationController?.navigationBar.tintColor = .white
    }

遷移先のDetailViewController.swif

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        transitionCoordinator?.animate(alongsideTransition: { _ in
            self.navigationController?.navigationBar.tintColor = .red
            self.navigationController?.navigationBar.barTintColor = .yellow
        }, completion: nil)
    }

といったようにやると、Pushなどの遷移中に遷移の比率に応じてアニメーションで色が変わっていくの良い。 シュミレーターの[Debug]-[Slow Animations]をONにして確かめると一目瞭然。

エッジスワイプバックのキャンセル対応

エッジスワイプで前の画面に戻ろうとしてキャンセルする操作をすると、ライフサイクル的に呼び出し元の画面のviewWillAppearが呼ばれ、現在表示されている遷移先画面はviewDidAppearが呼ばれるので、この実装だとエッジスワイプバックのキャンセルをすると、遷移元であるMainViewControllerの設定がDetailViewControllerに反映されてしまう。

f:id:hachinobu:20180721213919p:plainf:id:hachinobu:20180721213930p:plain
スワイプバックによる設定崩れ

なのでDetailViewControllerでtransitionCoordinator?.animateメソッドのcompletionブロックで、この画面の完成形となる色設定などを書いてあげておくと、違う画面の設定になってしまうということを防げる。

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        transitionCoordinator?.animate(alongsideTransition: { _ in
            self.navigationController?.navigationBar.tintColor = .red
            self.navigationController?.navigationBar.barTintColor = .yellow
        }, completion: { _ in
            self.navigationController?.navigationBar.tintColor = .red
            self.navigationController?.navigationBar.barTintColor = .yellow
        })
    }

そのサイトがATS対応しているのかを調べる方法

背景

あれ? WebViewで外部サイトが開かないんですけど・・もしかしてみたいな状況

やり方

nscurlコマンドというものを知った

これを使うとそのドメインのサイトがATSに対応しているか、iOS側でどういった設定をすれば通るようになるのかを教えてくれる。

nscurl --ats-diagnostics --verbose https://www.google.co.jp/

みたいに叩くと

Starting ATS Diagnostics

Configuring ATS Info.plist keys and displaying the result of HTTPS loads to https://www.google.co.jp/.
A test will "PASS" if URLSession:task:didCompleteWithError: returns a nil error.
================================================================================

Default ATS Secure Connection
---
ATS Default Connection
ATS Dictionary:
{
}
Result : PASS
---

================================================================================

Allowing Arbitrary Loads

---
Allow All Loads
ATS Dictionary:
{
    NSAllowsArbitraryLoads = true;
}
Result : PASS
---

================================================================================

Configuring TLS exceptions for www.google.co.jp

---
TLSv1.3
ATS Dictionary:
{
    NSExceptionDomains =     {
        "www.google.co.jp" =         {
            NSExceptionMinimumTLSVersion = "TLSv1.3";
        };
    };
}
2018-07-18 22:57:15.859 nscurl[36979:1099485] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9800)
Result : FAIL
Error : Error Domain=NSURLErrorDomain Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made." UserInfo={_kCFStreamErrorCodeKey=-9800, NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, NSUnderlyingError=0x7fac10714b10 {Error Domain=kCFErrorDomainCFNetwork Code=-1200 "(null)" UserInfo={_kCFStreamPropertySSLClientCertificateState=0, _kCFNetworkCFStreamSSLErrorOriginalValue=-9800, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9800}}, NSLocalizedDescription=An SSL error has occurred and a secure connection to the server cannot be made., NSErrorFailingURLKey=https://www.google.co.jp/, NSErrorFailingURLStringKey=https://www.google.co.jp/, _kCFStreamErrorDomainKey=3}
---

---
TLSv1.2
ATS Dictionary:
{
    NSExceptionDomains =     {
        "www.google.co.jp" =         {
            NSExceptionMinimumTLSVersion = "TLSv1.2";
        };
    };
}
Result : PASS
---

---
TLSv1.1
ATS Dictionary:
{
    NSExceptionDomains =     {
        "www.google.co.jp" =         {
            NSExceptionMinimumTLSVersion = "TLSv1.1";
        };
    };
}
Result : PASS
---

---
TLSv1.0
ATS Dictionary:
{
    NSExceptionDomains =     {
        "www.google.co.jp" =         {
            NSExceptionMinimumTLSVersion = "TLSv1.0";
        };
    };
}
Result : PASS
---

================================================================================

Configuring PFS exceptions for www.google.co.jp

---
Disabling Perfect Forward Secrecy
ATS Dictionary:
{
    NSExceptionDomains =     {
        "www.google.co.jp" =         {
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : PASS
---

================================================================================

Configuring PFS exceptions and allowing insecure HTTP for www.google.co.jp

---
Disabling Perfect Forward Secrecy and Allowing Insecure HTTP
ATS Dictionary:
{
    NSExceptionDomains =     {
        "www.google.co.jp" =         {
            NSExceptionAllowsInsecureHTTPLoads = true;
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : PASS
---

================================================================================

Configuring TLS exceptions with PFS disabled for www.google.co.jp

---
TLSv1.3 with PFS disabled
ATS Dictionary:
{
    NSExceptionDomains =     {
        "www.google.co.jp" =         {
            NSExceptionMinimumTLSVersion = "TLSv1.3";
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
2018-07-18 22:57:16.828 nscurl[36979:1099496] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9800)
Result : FAIL
Error : Error Domain=NSURLErrorDomain Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made." UserInfo={_kCFStreamErrorCodeKey=-9800, NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, NSUnderlyingError=0x7fac1071b870 {Error Domain=kCFErrorDomainCFNetwork Code=-1200 "(null)" UserInfo={_kCFStreamPropertySSLClientCertificateState=0, _kCFNetworkCFStreamSSLErrorOriginalValue=-9800, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9800}}, NSLocalizedDescription=An SSL error has occurred and a secure connection to the server cannot be made., NSErrorFailingURLKey=https://www.google.co.jp/, NSErrorFailingURLStringKey=https://www.google.co.jp/, _kCFStreamErrorDomainKey=3}
---

---
TLSv1.2 with PFS disabled
ATS Dictionary:
{
    NSExceptionDomains =     {
        "www.google.co.jp" =         {
            NSExceptionMinimumTLSVersion = "TLSv1.2";
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : PASS
---

---
TLSv1.1 with PFS disabled
ATS Dictionary:
{
    NSExceptionDomains =     {
        "www.google.co.jp" =         {
            NSExceptionMinimumTLSVersion = "TLSv1.1";
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : PASS
---

---
TLSv1.0 with PFS disabled
ATS Dictionary:
{
    NSExceptionDomains =     {
        "www.google.co.jp" =         {
            NSExceptionMinimumTLSVersion = "TLSv1.0";
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : PASS
---

================================================================================

Configuring TLS exceptions with PFS disabled and insecure HTTP allowed for www.google.co.jp

---
TLSv1.3 with PFS disabled and insecure HTTP allowed
ATS Dictionary:
{
    NSExceptionDomains =     {
        "www.google.co.jp" =         {
            NSExceptionAllowsInsecureHTTPLoads = true;
            NSExceptionMinimumTLSVersion = "TLSv1.3";
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
2018-07-18 22:57:17.450 nscurl[36979:1099485] NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9800)
Result : FAIL
Error : Error Domain=NSURLErrorDomain Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made." UserInfo={_kCFStreamErrorCodeKey=-9800, NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, NSUnderlyingError=0x7fac10720c30 {Error Domain=kCFErrorDomainCFNetwork Code=-1200 "(null)" UserInfo={_kCFStreamPropertySSLClientCertificateState=0, _kCFNetworkCFStreamSSLErrorOriginalValue=-9800, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9800}}, NSLocalizedDescription=An SSL error has occurred and a secure connection to the server cannot be made., NSErrorFailingURLKey=https://www.google.co.jp/, NSErrorFailingURLStringKey=https://www.google.co.jp/, _kCFStreamErrorDomainKey=3}
---

---
TLSv1.2 with PFS disabled and insecure HTTP allowed
ATS Dictionary:
{
    NSExceptionDomains =     {
        "www.google.co.jp" =         {
            NSExceptionAllowsInsecureHTTPLoads = true;
            NSExceptionMinimumTLSVersion = "TLSv1.2";
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : PASS
---

---
TLSv1.1 with PFS disabled and insecure HTTP allowed
ATS Dictionary:
{
    NSExceptionDomains =     {
        "www.google.co.jp" =         {
            NSExceptionAllowsInsecureHTTPLoads = true;
            NSExceptionMinimumTLSVersion = "TLSv1.1";
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : PASS
---

---
TLSv1.0 with PFS disabled and insecure HTTP allowed
ATS Dictionary:
{
    NSExceptionDomains =     {
        "www.google.co.jp" =         {
            NSExceptionAllowsInsecureHTTPLoads = true;
            NSExceptionMinimumTLSVersion = "TLSv1.0";
            NSExceptionRequiresForwardSecrecy = false;
        };
    };
}
Result : PASS
---

================================================================================

といった感じで、TLSのver.いくつとか、暗号化スイートの設定とか、どういった設定なら通るよというのが分かってめっちゃ便利。

各設定の意味とかは公式ドキュメント

developer.apple.com

参考

教えてもらった

blog.kishikawakatsumi.com

管理者権限ないのでHomebrewでrbenvを入れたけどrbenv execを省略できなくてハマったメモ

背景

管理者権限ない状態でgem install bundler叩いてもPermissionErrorになるのは周知の通り なのでrbenv経由でruby入れるとユーザー領域にgemをインストールできるよってことで下記をやってみたが、コンソール再起動すると、bundleコマンドを叩いてもコマンドないよーって言われる始末。

管理者権限なしでgemをインストールする

もちろん rbenv exec bundle exec hogehogeとかは大丈夫。

解決方法

上記の参考にした記事で

if which rbenv> /dev/null; then eval "$(rbenv init -)"; fi

source ~/.bash_profile

とかコマンド叩いてるので良い感じに.bash_profileが更新されると思ってたけどされてなかった。

試しに rbenv init コマンド叩いてみると下記が出力

# Load rbenv automatically by appending
# the following to ~/.bash_profile:

eval "$(rbenv init -)"

ということで.bash_profileeval "$(rbenv init -)"を追記してsource ~/.bash_profile叩いてbundle コマンドを実行してみると無事に動きました (もちろんコンソール再起動しても)

このあたりのスキルなさすぎてハマりすぎて引いた・・

WKWebViewのscrollの高さを動的に読み込む方法

背景

セルにWKWebView引いてコンテンツを読み込ませて、読み込み後のスクロールの高さを取得して、セルの高さにしたかったけど色々つまったので備忘録

解決方法

StackOverFlowに載ってる方法でほぼ問題なし

How to determine the content size of a WKWebView?

だけど、1点だけうまくいかなかったのが、高さを取得する時に navigationDelegatefunc webView(_ webView: WKWebView, didFinish navigation: WKNavigation!)メソッド内で高さの取得を

webView.evaluateJavaScript("document.body.offsetHeight", completionHandler: { (height, error) in
                            
})

で取得すると、正しい高さよりも激しく大きな数値が返ってきてしまい、スクロールすると余白だらけになってしまった。

なので、

webView.scrollView.contentSize.height

で取得するようにしたら正しいのが取れた

func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
        
        webView.evaluateJavaScript("document.readyState", completionHandler: { (complete, error) in
            if complete != nil {
                print(webView.scrollView.contentSize) //正しいの取れる
            }
        })
}

RuntimeError - [Xcodeproj] Unknown object version. を解消する

背景

Xcode 9.3 betaを使ってPodfile作って、いつものように pod install を叩くと RuntimeError - [Xcodeproj] Unknown object version. とエラーが出た。

色々調べてみるとCocoaPodsのリポジトリにissue立ってて解決されていた (#7458)https://github.com/CocoaPods/CocoaPods/issues/7458

解決方法

Rubyから*.xcodeprojファイルをいじることができるgemであるxcodeprojをインストールすれば良い。

[sudo] gem install xcodeproj

これをインストール後に pod install を叩いたらxcodeprojファイルをゴニョゴニョ最適なフォーマットにしてくれてPods経由のライブラリも正常にインストールできました

今更だけどSwiftの型消去の使い所が少し分かったので備忘録

背景

2016年のtry!Swiftで話題になった型消去

なかなか使い所が分かってなかったんだけど、利用したいシーンに出会ったので備忘録を記す

説明

前提

私は、開発する上で、よくHTTPクライアントのライブラリであるAPIKitを使っています。

APIKitの使い方を簡単に説明すると、Requestというプロトコルに準拠したクラスを書いて、APIKitのSessionクラスのsendメソッドの引数に指定することで、指定したエンドポイントにアクセスして結果を返してくれます。

このAPIKitのRequestプロトコルは、エンドポイントにアクセスして取得するレスポンスの型をassociatedtypeを使って、Requestプロトコルに準拠したクラスで指定できるようになっています。

public protocol Request {
    /// The response type associated with the request type.
    associatedtype Response

    //その他、HttpMethodやpathやパラメータなどのプロパティがある
    //....
}

下記は、あるAPIを叩いて成功した場合にStringの配列を返すクラスです。

public class HogeEndpoint: Request {
    typealias Response = [String]

    //...
}

実際に使う場合は下記のような感じになります

let request = HogeEndpoint()
Session(request) { result in
    switch result {
    case .success(let items):
        //Stringの配列を取得
    case .failure(let error):
        //Error処理
    }
}

型消去を使って解決した課題

APIKitの説明を踏まえた上で、Requestプロトコルに準拠したクラスで、APIのエンドポイントは違うけれども成功した時に返ってくる値の型は同じであるパターンを想定してみます。

例えば下記のようなHogeEndpointFugaEndpointクラスです。

class HogeEndpoint: Request {
    typealias Response = [String]
    
    var path: String = "/api/v1/hoge"
    //...
}

class FugaEndpoint: Request {
    typealias Response = [String]

    var path: String = "/api/v1/fuga"
    //...
}

これは、接続するエンドポイントは違いますが、結果として得られる値の型は同じです。

そこでString型の配列をリストで表示する画面を作ることを想定してみましょう。

リスト表示のViewControllerにRequestプロトコルのStored propertyを宣言してやれば、ViewControllerが汎用的に使えると思い下記のようなコードを書きました。

import UIKit
import APIKit

class StringListTableViewController: UITableViewController {

    var request: Request!

    private var items: [String]? {
        didSet {
            tableView.reloadData()
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "reuseIdentifier")
        fetchItem()
    }
    
    private func fetchItem() {
        
        Session.send(request) { [weak self] result in
            switch result {
            case .success(let items):
                self?.items = items
            case .failure(let error):
                print(error)
            }
        }
        
    }

    // MARK: - Table view data source
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items?.count ?? 0
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)
        cell.textLabel?.text = items?[indexPath.row]
        return cell
    }        

}

これで色々な画面から、String配列のリスト表示画面であるStringListTableViewControllerのStored propertyであるrequestに値を渡すだけで汎用的に使えると思うかもしれません。

しかし、これではProtocol 'Request' can only be used as a generic constraint because it has Self or associated type requirements.エラーになります。

f:id:hachinobu:20171115200204p:plain

Requestプロトコルassociatedtypeの型が決まってないのでダメよってことですね。

当然といえば当然です。

ならばRequestプロトコルジェネリクスを使って下記のようにすればと思いますが残念ながらSwiftはプロトコルでのジェネリクスをサポートしていません。

public protocol Request<Result> {
    /// The response type associated with the request type.
    associatedtype Response

    //その他、HttpMethodやpathやパラメータなどのプロパティがある
    //....
}

extension Request {
    typealias Response = Result
}

Protocols do not allow generic parameters; use associated types instead

では、こんな時はどうすれば良いでしょうか?

結局、Requestプロトコルassociatedtypeに引きづられて、同じようなViewを作るしかないのでしょうか?

いや待ってください。そうです。こんな時に型消去です。

型消去

(型消去自体の説明は沢山良記事があるので割愛します)

このエラーを回避してViewControllerを汎用的に使い回すために下記のような型消去クラスを作成します

struct TypeEraseEndpoint<ResponseType>: Request {

    typealias Response = ResponseType
    
    var path: String
    var method: HTTPMethod
    private let responseHandler: ((Any, HTTPURLResponse) throws -> ResponseType)
    
    init<R: Request>(request: R) where R.Response == ResponseType {
        self.path = request.path
        self.queryParameters = request.queryParameters
        self.method = request.method
        self.dataParser = request.dataParser
        self.responseHandler = request.response
    }
    
    func response(from object: Any, urlResponse: HTTPURLResponse) throws -> ResponseType {
        return try responseHandler(object, urlResponse)
    }

}

このような型消去のクラスを使うことで、先ほどのStringListTableViewControllerのStored propertyであるrequestの型をTypeEraseEndpoint<[String]>!に変更することで、ViewControllerを汎用的に使うことができました。

import UIKit
import APIKit

class StringListTableViewController: UITableViewController {

    var request: TypeEraseEndpoint<[String]>!

    private var items: [String]? {
        didSet {
            tableView.reloadData()
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "reuseIdentifier")
        fetchItem()
    }
    
    private func fetchItem() {
        
        Session.send(request) { [weak self] result in
            switch result {
            case .success(let items):
                self?.items = items
            case .failure(let error):
                print(error)
            }
        }
        
    }

    // MARK: - Table view data source
    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return items?.count ?? 0
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)
        cell.textLabel?.text = items?[indexPath.row]
        return cell
    }        

}

この型消去のサンプルプロジェクトをQiitaのAPIを使って作ってみました。

SwiftTypeEraseSample

もしよければ触ってみてください。

ただし、TypeEraseのジェネリクスにProtocol型を指定して柔軟にとかは出来ない・・

最後に

そもそもCleanArchitectureみたいなレイヤードアーキテクチャを意識して作れば、このような場面には出くわさないんですけどね。

ViewControllerにRequestプロトコル渡すような書き方するなよとか言わないで!!

とはいえ、ほんと今更の理解で凹む。。

普段からassociatedTypeのProtocolを書いていないという設計力の低さを露呈しているような気がする。