Xcode
framework
Swift

【Swift】Frameworkを作成する

1.はじめに

 いくつかプロジェクトを作成しているとコードを使いまわしたいことが出てくると思います。そんな時に便利なのがフレームワーク。一度作成したら、各プロジェクトにインポートするだけで使えるようになります。
 フレームワークの作成、デバッグ方法をまとめたいと思います。

2.環境

Xcode Version 9.2
Swift Version 4.0.3

3.前提

当記事ではプロジェクト名を以下の通りにしています
フレームワーク : Sally
フレームワークを使用するプロジェクト : Jack

4.フレームワークの作成

File > New > Projectから「Cocoa Touch Framework」を選択。
「Product Name」を「Sally」にして作成。

「Sally」フォルダの中にファイルを追加します。このファイルにフレームワークとして作成したい機能を実装します。
File > New > File から「Cocoa Touch Class」を選択。
クラス名を「Sally」にします。
フォルダ構成はこのようになります。
スクリーンショット

以下のように作成したクラスに機能を実装していきます。
ポイントはクラス、メソッドともに public にすることです。
public にすることでフレームワーク外からアクセスできるようにします。

// Sally.swift

public class Sally: NSObject {
    public func sallyMethod() {
        print("Sally().sallyMethod is called !!")
    }
}

ビルドします。ビルドすることでプロジェクトで使用できる状態のフレームワークを生成します。
ポイントはシミュレーター用と実機用で別々に作成する必要があることです。
シミュレーター、実機共通のフレームワークの作成も後々説明しますが、その場合も一度それぞれのフレームワークを作成する必要があります。

まずは実機用のフレームワークを作成します。
ターゲットに実機か「Generic iOS Device」を選択。
ビルド前はまだファイルがないので「Sally.framework」が赤字になっています。
スクリーンショット

「command + B」 でビルドします。
ビルドに成功すると「Sally.framework」が黒字になります。
スクリーンショット

次にシミュレーター用のフレームワークを作成します。
ターゲットにシミュレーター(iPhone X でもiPhone 8 Plusでもなんでもいいです)を選択。
実機用を作成した時と同様に「command + B」でビルドします。

作成したフレームワークは「Sally.framework」を選択して「Show in Finder」で確認することができます。
スクリーンショット

「Debug-iphoneos」というフォルダ内のフレームワークが実機用、「Debug-iphonesimulator」というフォルダ内がシミュレーター用です。
スクリーンショット

5.フレームワークを使用する

ここまでで作ったフレームワークを使ってみます。
File > New > Project を選択し、「Single View App」でプロジェクトを新規作成します。
プロジェクト名は「Jack」にしました。

プロジェクトに先ほど作成したフレームワークをインポートします。
「Jack」をシミューレーターで確認する場合は「Debug-iphonesimulator」フォルダ内のフレームワークを、実機で確認する場合は「Debug-iphoneos」フォルダ内のフレームワークをドラッグ&ドロップでインポートします。
スクリーンショット

「Copy Items if needed」をチェックしない場合、フレームワークのコピーは取り込まれずリンクがされます。
この場合、「Build Setting」の「Search Paths」> 「Framework Search Paths」に適切なパスが設定されていないとビルドできません。
スクリーンショット

インポートしたフレームワークのパスを設定します。
スクリーンショット

次にインポートしたフレームワークを「Embedded Binaries」にドラッグします。
この時、「Linked Frameworks and Libraries」にも追加されてしまうので、削除します。
スクリーンショット 2018-01-06 13.28.13.png

それではフレームワークの機能を使ってみます。
Sally フレームワークをインポートし、 viewDidLoad 内でフレームワークのメソッドを実行します。

//ViewController.swift

import UIKit
import Sally

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        Sally().sallyMethod()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

ランしてみましょう。
コンソールに「Sally().sallyMethod is called !!」と表示されれば成功です!

6.シミュレーターと実機の共通のフレームワークを作成する

先ほど作成したフレームワークはシミュレーター用と実機用とで別々でした。フレームワークを使用するプロジェクトをどちらで確認するかによってインポートするフレームワークを変える必要があります。
そこで、共通のフレームワークを作成することにします。

作業するプロジェクトは再び「Sally」です。
「Sally」に新しいターゲットを追加します。
File > New > Target から「Aggregate」を選択。
スクリーンショット

プロジェクト名を「Sally-Universal」にしました。

次に「Sally-Universal」の「Build Phases」で「+」のアイコンから「New Run Script Phase」を選択。
スクリーンショット 2018-01-06 13.43.30.png

スクリプトをコピペします。
スクリーンショット 2018-01-06 13.46.42.png

######################
# Options
######################

REVEAL_ARCHIVE_IN_FINDER=false

FRAMEWORK_NAME="${PROJECT_NAME}"

SIMULATOR_LIBRARY_PATH="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${FRAMEWORK_NAME}.framework"

DEVICE_LIBRARY_PATH="${BUILD_DIR}/${CONFIGURATION}-iphoneos/${FRAMEWORK_NAME}.framework"

UNIVERSAL_LIBRARY_DIR="${BUILD_DIR}/${CONFIGURATION}-iphoneuniversal"

FRAMEWORK="${UNIVERSAL_LIBRARY_DIR}/${FRAMEWORK_NAME}.framework"


######################
# Build Frameworks
######################

xcodebuild -workspace ${PROJECT_NAME}.xcworkspace -scheme ${PROJECT_NAME} -sdk iphonesimulator -configuration ${CONFIGURATION} clean build CONFIGURATION_BUILD_DIR=${BUILD_DIR}/${CONFIGURATION}-iphonesimulator 2>&1

xcodebuild -workspace ${PROJECT_NAME}.xcworkspace -scheme ${PROJECT_NAME} -sdk iphoneos -configuration ${CONFIGURATION} clean build CONFIGURATION_BUILD_DIR=${BUILD_DIR}/${CONFIGURATION}-iphoneos 2>&1

######################
# Create directory for universal
######################

rm -rf "${UNIVERSAL_LIBRARY_DIR}"

mkdir "${UNIVERSAL_LIBRARY_DIR}"

mkdir "${FRAMEWORK}"


######################
# Copy files Framework
######################

cp -r "${DEVICE_LIBRARY_PATH}/." "${FRAMEWORK}"


######################
# Make an universal binary
######################

lipo "${SIMULATOR_LIBRARY_PATH}/${FRAMEWORK_NAME}" "${DEVICE_LIBRARY_PATH}/${FRAMEWORK_NAME}" -create -output "${FRAMEWORK}/${FRAMEWORK_NAME}" | echo

# For Swift framework, Swiftmodule needs to be copied in the universal framework
if [ -d "${SIMULATOR_LIBRARY_PATH}/Modules/${FRAMEWORK_NAME}.swiftmodule/" ]; then
cp -f ${SIMULATOR_LIBRARY_PATH}/Modules/${FRAMEWORK_NAME}.swiftmodule/* "${FRAMEWORK}/Modules/${FRAMEWORK_NAME}.swiftmodule/" | echo
                                                                      fi

                                                                      if [ -d "${DEVICE_LIBRARY_PATH}/Modules/${FRAMEWORK_NAME}.swiftmodule/" ]; then
                                                                      cp -f ${DEVICE_LIBRARY_PATH}/Modules/${FRAMEWORK_NAME}.swiftmodule/* "${FRAMEWORK}/Modules/${FRAMEWORK_NAME}.swiftmodule/" | echo
                                                                      fi

                                                                      ######################
                                                                      # On Release, copy the result to release directory
                                                                      ######################
                                                                      OUTPUT_DIR="${PROJECT_DIR}/Output/${FRAMEWORK_NAME}-${CONFIGURATION}-iphoneuniversal/"

                                                                      rm -rf "$OUTPUT_DIR"
                                                                      mkdir -p "$OUTPUT_DIR"

                                                                      cp -r "${FRAMEWORK}" "$OUTPUT_DIR"

                                                                      if [ ${REVEAL_ARCHIVE_IN_FINDER} = true ]; then
                                                                      open "${OUTPUT_DIR}/"
                                                                      fi

ターゲットを「Sally-Universal」にしてビルドをしてフレームワークを作成します。
スクリーンショット

先ほど別々のフレームワークを作成した時と同様に「Show in Finder」で作成したフレームワークを確認します。
「Debug-iphoneuniversal」というフォルダに作成されたことが確認できます。
スクリーンショット

このフレームワークを「Jack」プロジェクトにインポートすることでシミュレーターでも実機でも同一のフレームワークが使えるようになります。

7.フレームワークとアプリを一緒に作成する

後々別のアプリでも使用することが想定される場合、アプリと同時にフレームワークを作成してデバッグしたい。そんなことができたら便利!!
今回は「Jack」プロジェクトで同時に「Sally」フレームワークを作成します。

まず、「Jack」プロジェクトから「Sally」フレームワークを削除しておきます。

「Add Files to "Jack"...」を選択。
スクリーンショット

「Sally.xcodeproj」を選択すると、こんな感じのフォルダ構成になります。
先ほど確認した場所とは違う場所にフレームワークを格納するようで、「Sally.framework」が赤字になっています。
スクリーンショット

ターゲットを「Sally」にしてシミュレーター、実機用それぞれビルド。
ターゲットを「Sally-Universal」にしてビルド。
スクリーンショット 2018-01-06 14.18.08.png

ここで新しく作成したUniversalな「Sally」フレームワークを「Jack」プロジェクトに取り込みます。
「Embedded Binaries」への追加と「Build Setting」の「Framework Search Paths」への追加をお忘れなく。

さて、フレームワークの更新をしてみます。
Sally クラスに sallyMethod2 を追加します。

// Sally.swift

import UIKit

public class Sally: NSObject {
    public func sallyMethod() {
        print("Sally().sallyMethod is called !!")
    }
    public func sallyMethod2() {
        print("Sally().sallyMethod2 is called !!")
    }
}

これまでと同様に実機用ビルド、シミュレーター用ビルド、Universal用ビルドを実行します。
「Jack.swift」の Jack.swiftviewDidLoadSally().sallyMethod2() を追加します。

「Jack」プロジェクトでフレームワークの機能を使用します。

// Jack.swift

import UIKit
import Sally

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        Sally().sallyMethod()
        Sally().sallyMethod2()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
}

Sally クラスの sallyMethod2 でブレークポイントを設定してから、「Jack」をランします。
スクリーンショット

設定したブレークポイントで止まれば成功!!
スクリーンショット

これでアプリもフレームワークも同時に作成できますね。

8.最後に

フレームワークの作成が終わったら、「Sally.xcodeproj」を削除します。
スクリーンショット

「Sally.xcodeproj」単体で開いてビルドしてフレームワークを生成します。
生成したフレームワークを「Jack」プロジェクトに取り込めば、「Jack」プロジェクトから「Sally」プロジェクトを削除した上で、フレームワークを使用することができます。

9.課題というか疑問

生成したフレームワークは「Debug-○○○」というフォルダに格納されます。
「Debug」。。。ものすごく気になります。
アプリをリリースする際はこのフレームワークもリリース用にビルドしなければならないのかどうか。
今後試してみたいと思います。

10.参考

[Xcode6] SwiftでCocoa Touch Frameworkを作る
[Xcode6] Universal Frameworkを作る
[Xcode6] iOSアプリプロジェクト内にframeworkプロジェクト
Xcode7.3 & SwiftでつくるCocoaTouch Framework(作成編)

間違っている点や改善点などありましたら、ぜひフィードバックをお願いします。