しめ鯖日記

swift, iPhoneアプリ開発, ruby on rails等のTipsや入門記事書いてます

gRPCをSwiftで使ってみる

gRPCを使ってgoのサーバーにSwiftでアクセスする処理を実装しました。
Swiftは5.2、Go言語はgo1.14.4、protobufは3.12.3を使っています。

gRPCとは

gRPCとはGoogleが開発したRPCを実現するためのプロトコルのことです。
RPCはremote procedure callの略で、別のサーバーのメソッドなどを呼び出すことができる技術です。

要するにgRPCを使えば今までRESTなどでAPI通信した箇所を置き換えることができます。

今回はSwiftからgRPCを使ってサーバーにアクセスしてみたいと思います。

Go言語でgRPCサーバーを作る

サーバー立ち上げは下サイトを参考にさせて頂きました。

https://tech.smartcamp.co.jp/entry/2019/03/28/175137

Go言語とprotobufが入っていない場合はbrewでインストールします。
protobufは

brew install go
brew install protobuf

下コマンドで必要なライブラリをインストールします。

go get -u google.golang.org/grpc
go get -u github.com/golang/protobuf/protoc-gen-go

データ構造を示すprotoファイルを作成します。
pingerというフォルダを作ってpinger.protoというファイルを作って下のように記述します。

syntax = "proto3";

package pinger;

service Pinger {
  rpc Ping(Empty) returns (Pong) {}
}

message Empty {}

message Pong {
  string text = 1;
}

protoファイルを作ったら、下コマンドでgo言語のファイルを生成します。

protoc ./pinger/pinger.proto --go_out=plugins=grpc:.

もし下エラーになった場合、goのライブラリへのパスが足りていない可能性があります。
PATH="${PATH}:${HOME}/go/bin"でパスを追加すると解消することがあります。

protoc-gen-go: program not found or is not executable
Please specify a program using absolute path or make sure the program is available in your PATH system variable
--go_out: protoc-gen-go: Plugin failed with status code 1.

protocコマンドがうまく動けばpingerフォルダにpinger.pb.goというファイルが生成されます。

f:id:llcc:20200621212321p:plain

最後にserverを作成します。
カレントディレクトリにserver.goというファイルを作って下のように記述します。

package main

import (
    "context"
    "log"
    "net"

    "./pinger"
    "google.golang.org/grpc"
)

func main() {
    listener, err := net.Listen("tcp", ":5300")
    if err != nil {
        log.Fatalf("failed to listen: %v\n", err)
        return
    }
    grpcSrv := grpc.NewServer()
    pinger.RegisterPingerServer(grpcSrv, &server{})
    log.Printf("Pinger service is running!")
    grpcSrv.Serve(listener)
}

type server struct{}

func (s *server) Ping(ctx context.Context, req *pinger.Empty) (*pinger.Pong, error) {
    pong := &pinger.Pong{
        Text: "pong",
    }
    return pong, nil
}

ファイルを作ってから下コマンドを実行するとサーバーが起動します。

go run server.go

起動したら動作確認をします。
今いるディレクトリに下のようなcommand.goを作成します。

package main

import (
    "context"
    "fmt"
    "os"

    "./pinger"
    "google.golang.org/grpc"
)

func main() {
    conn, err := grpc.Dial("localhost:5300", grpc.WithInsecure())
    if err != nil {
        fmt.Fprintf(os.Stderr, "grpc.Dial: %v\n", err)
        return
    }
    defer conn.Close()
    client := pinger.NewPingerClient(conn)
    req := &pinger.Empty{}
    pong, err := client.Ping(context.Background(), req)
    if err != nil {
        fmt.Fprintf(os.Stderr, "Ping: %v\n", err)
        return
    }
    fmt.Fprintf(os.Stdout, "Pong: %s\n", pong.Text)
}

下コマンドを実行してPong: pongという表示になれば問題なく動いています。

go run command.go

Swiftからサーバーのメソッドを呼び出す

次はSwiftでクライアントを作ります。
適当なプロジェクトを作成したらCocoaPodsでgRPC-Swiftをインストールします。

target 'MyApp' do
  use_frameworks!

  pod 'gRPC-Swift', '1.0.0-alpha.12'
end

次は下コマンドでprotocのswiftプラグインをインストールします。

git clone https://github.com/grpc/grpc-swift.git
cd grpc-swift
make plugins

生成したプラグインはパスの通っている場所に移動します。
今回は一旦試したいだけなのでgrpc-swiftのパスを追加するだけにしました。

PATH="${PATH}:${HOME}/XXXX/grpc-swift"

ここまで終わったらクライアント用のファイルを生成します。

protoc pinger/pinger.proto --swift_out=. --grpc-swift_out=.

pinger.pb.swiftpinger.grpc.swiftというファイルが作られるので、これをプロジェクトに追加します。

追加したら実際に通信を行います。
AppDelegateを下のように修正します。

import UIKit
import GRPC
import NIO

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        let group = MultiThreadedEventLoopGroup(numberOfThreads: 1)
        let channel = ClientConnection.insecure(group: group).connect(host: "localhost", port: 5300)
        let client = Pinger_PingerClient(channel: channel)
        let request = Pinger_Empty()
        let call = client.ping(request)
        print(try! call.response.wait())
        
        return true
    }
}

実行すると下のようにレスポンスが返る事がわかります。

f:id:llcc:20200628171237p:plain

参考URL

遠隔手続き呼出し - Wikipedia

iOSエンジニアの為のgrpc-swift入門 - Speaker Deck

SwiftUIのGeometryReaderを試してみる

SwiftUIのGeometryReaderという構造体を使ってみました。
GeometryReaderとは自身の座標やサイズを子Viewに渡せる構造体です。
これを使う事で、動的に変化するViewなどが作りやすくなります。

今回は下URLを参考にさせて頂きました。

qiita.com

参照URLにもある中心に行くほど大きくなるViewを作ってみます。
まずはSwiftUIのプロジェクトを作成します。

struct ContentView: View {
    var body: some View {
        Text("Hello world")
    }
}

次はScrollViewの上に10個の円を並べます。

struct ContentView: View {
    var body: some View {
        ScrollView(.horizontal) {
            HStack {
                ForEach(0..<10) {_ in
                    Circle().frame(width: 100, height: 100).foregroundColor(Color.gray)
                }
            }
        }
    }
}

実行すると下のように円が並びます。

f:id:llcc:20200607154829p:plain

次は中心に来たときに大きくなるような実装をします。
ここでGeometryReaderを使います。

実装は下のとおりです。
CircleをGeometryReaderで囲む事でCircleの位置が分かるようにしています。

geometry.frame(in: .global)でCircleの位置を把握したら、それを使って円のサイズを変更しています。

struct ContentView: View {
    var body: some View {
        ScrollView(.horizontal) {
            HStack {
                ForEach(0..<10) {_ in
                    GeometryReader { geometry in
                        Circle().foregroundColor(Color.gray)
                            .scaleEffect(self.scale(x: geometry.frame(in: .global).midX))
                    }.frame(width: 100, height: 200)
                }
            }
        }
    }
    
    func scale(x: CGFloat) -> CGFloat {
        let centerX = UIScreen.main.bounds.width / 2
        return max(1.5 - abs(x - centerX) / centerX, 1)
    }
}

実行すると下のように中心の円が大きくなります。

f:id:llcc:20200607160519p:plain

XcodeGenでxcodeprojファイルをyamlで管理する

XcodeGenというxcodeprojファイルをyaml管理できるツールを試してみました。
これを使う事でxcodeprojファイルのコンフリクトを避ける事ができるようになります。

github.com

インストール

今回はbrewを使ってインストールしました。

brew install xcodegen

初期設定

ルートディレクトリにproject.ymlを設置するだけで使う事ができます。

name: MyApp
options:
  bundleIdPrefix: com.myApp
targets:
  MyApp:
    type: application
    platform: iOS
    deploymentTarget: "13.0"
    sources: MyApp

yamlファイルを置いたら下コマンドを実行します。

xcodegen generate

実行すると以下のようにMyApp.xcodeprojファイルが生成されます。

f:id:llcc:20200531192816p:plain

使い方

Carthageを使っている場合、下のように依存関係を記載する事ができます。
CocoaPodsの場合はPodfileを使う必要があります。

targets:
  MyApp:
    dependencies:
      - carthage: Kingfisher

Run Scriptの追加をする事もできます。

targets:
  MyApp:
    preBuildScripts:
      - script: printf hello

xcodegen generateを実行すると下のように追加されます。

f:id:llcc:20200531194458p:plain

その他の利用可能オプションは下URLに詳しく記載されています。

github.com

nmコマンドで.frameworkにUIWebViewが入っているか確認する

nmコマンドでUIWebViewを使っているか判別できるという事で検証してみました。
参考にしたのは下サイトです。

qiita.com

まずは下記事を参考に自作のframeworkを作ります。

llcc.hatenablog.com

作成したframeworkに下のようなクラスを1つ追加します。

import UIKit

public class MyClass {
}

追加したらビルドしてframeworkを生成します。
生成したframeworkはProductsグループの中に入っています。

f:id:llcc:20200525220616p:plain

実際に下コマンドでUIWebViewが含まれているか確かめてみます。

nm MyFramework.framework/MyFramework | grep UIWeb

実行結果は下の通りです。
UIWebViewを使っていないので何も出力されませんでした。

f:id:llcc:20200525220804p:plain

次は実際にUIWebViewを使うとどうなるか確かめてみます。
先程作ったクラスで下のようにUIWebViewのインスタンスを生成します。

public class MyClass {
    func test() {
        let webView = UIWebView()
    }
}

ビルドしてコマンド実行した結果は以下の通りです。
無事にUIWebViewを使っているかどうか判別できました。

f:id:llcc:20200525221023p:plain

UIWebViewは以下のようにprivateなクラスで使っている場合も検出できます。

private class MyClass {
    private func test() {
        let webView = UIWebView()
    }
}

Xcodeの[プロジェクト名].entitlementsというファイルについて調べてみる

Xcodeで自動的に作られる[プロジェクト名].entitlementsについて調べてみました。

[プロジェクト名].entitlementsとは

entitlementsとはプッシュ通知やApp Groupsなど、プロジェクトのCapabilityでの設定に関するファイルです。
Capabilityで通知の追加やApp Groupsのcontainer追加時に自動更新されます。

f:id:llcc:20200524212420p:plain

ファイルをXcodeで開くと下のようになっています。
App Groupsのコンテナ名やプッシュ通知の環境などが設定されています。

f:id:llcc:20200524213258p:plain

設定可能なキーは下のドキュメントで確認する事が可能です。

https://developer.apple.com/documentation/bundleresources/entitlements

entitlementsファイルはプロジェクト設定でパス指定する事でDebugとReleaseのファイルを分けることも可能です。

f:id:llcc:20200524222517p:plain

Realm Swift + SwiftUIでテーブル表示・編集・削除

Realm SwiftとSwiftUIを使ったテーブル実装を試してみました。

注) 2020/04/26時点でRealmはSwiftUIを公式サポートしていません。
対応状況は下Issueを参考にしてみて下さい。

github.com

表示

プロジェクトを作ったらCocoaPodsでRealmSwiftを追加します。

platform :ios, '13.0'

target 'MyApp' do
  use_frameworks!

  pod 'RealmSwift'
end

Realmの追加が終わったらEntityを作成します。
定義は下の通りです。

import UIKit
import RealmSwift

class MyModel: Object {
    @objc dynamic var id = UUID().uuidString
    @objc dynamic var title = ""
}

次はこのデータをテーブル表示します。
後々のことを考えて、データ一覧はViewModelに入れています。

import SwiftUI
import RealmSwift

struct ContentView: View {
    @ObservedObject var model = ContentViewModel()
    
    var body: some View {
        List {
            ForEach(model.myModels, id: \.id) { myModel in
                Button(action: {
                }) {
                    Text("\(myModel.title)")
                }
            }
        }
    }
}

class ContentViewModel: ObservableObject {
    @Published var myModels: [MyModel] = (try? Realm().objects(MyModel.self).map { $0 }) ?? []
}

テストデータを3件作ってから実行すると下のようになります。
日付をそのままtitleに入れたので下のような表示になっています。

f:id:llcc:20200426201026p:plain

編集

次はデータの更新をします。
Cellをタップしたら現在時刻をtitleにセットするような実装にしたいと思います。

まずはViewModeを以下のように修正します。
ここではRealmのobserveを使って更新を受け取るように変更しています。

class ContentViewModel: ObservableObject {
    private var token: NotificationToken?
    private var myModelResults = try? Realm().objects(MyModel.self)
    @Published var myModels: [MyModel] = []
    
    init() {
        token = myModelResults?.observe { [weak self] _ in
            self?.myModels = self?.myModelResults?.map { $0 } ?? []
        }
    }
    
    deinit {
        token?.invalidate()
    }
}

続けてView側も修正します。

struct ContentView: View {
    @ObservedObject var model = ContentViewModel()
    
    var body: some View {
        List {
            ForEach(model.myModels, id: \.id) { myModel in
                Button(action: {
                    try? Realm().write {
                        myModel.title = "\(Date())"
                    }
                }) {
                    Text("\(myModel.title)")
                }
            }
        }
    }
}

これで、ボタンをタップする度にtitleが更新されるようになりました。

追加

次はデータの追加を実装します。
ナビゲーションバーの追加ボタンを押したらデータが増えるような仕様にしたいと思います。

Viewを下のように変更します。
追加したのはnavigationBarItems以下の処理です。

struct ContentView: View {
    @ObservedObject var model = ContentViewModel()
    
    var body: some View {
        NavigationView {
            List {
                ForEach(model.myModels, id: \.id) { myModel in
                    Button(action: {
                        try? Realm().write {
                            myModel.title = "\(Date())"
                        }
                    }) {
                        Text("\(myModel.title)")
                    }
                }
            }.navigationBarItems(trailing: Button(action: {
                let myModel = MyModel()
                myModel.title = "\(Date())"
                let realm = try? Realm()
                try? realm?.write {
                    realm?.add(myModel)
                }
            }) {
                Text("Add")
            })
        }
    }
}

これでデータの追加ができるようになりました。

f:id:llcc:20200426203109p:plain

削除

最後に削除処理を実装します。
削除処理は下のようにonDeleteを付ける事で実現できます。

struct ContentView: View {
    @ObservedObject var model = ContentViewModel()
    
    var body: some View {
        NavigationView {
            List {
                ForEach(model.myModels, id: \.id) { myModel in
                    Button(action: {
                        try? Realm().write {
                            myModel.title = "\(Date())"
                        }
                    }) {
                        Text("\(myModel.title)")
                    }
                }.onDelete { indexSet in
                    if let index = indexSet.first {
                        let realm = try? Realm()
                        try? realm?.write {
                            realm?.delete(self.model.myModels[index])
                        }
                    }
                }
            }.navigationBarItems(trailing: Button(action: {
                let myModel = MyModel()
                myModel.title = "\(Date())"
                let realm = try? Realm()
                try? realm?.write {
                    realm?.add(myModel)
                }
            }) {
                Text("Add")
            })
        }
    }
}

実行すると削除ボタンが出るようになります。

f:id:llcc:20200426203427p:plain

ただ、削除しようとすると以下のようなエラーが出てしまいます。
Terminating app due to uncaught exception 'RLMException', reason: 'Object has been deleted or invalidated.'

これはデータを削除した後もSwiftUIが削除済みデータにアクセスするからです。
今回はcellの対になるViewModelを作る事で対応しました。
ViewModelの定義は下の通りです。

struct ContentViewCellModel {
    let id: String
    let title: String
}

View全体のViewModelもContentViewCellModelの配列を持つようにします。

class ContentViewModel: ObservableObject {
    private var token: NotificationToken?
    private var myModelResults = try? Realm().objects(MyModel.self)
    @Published var cellModels: [ContentViewCellModel] = []
    
    init() {
        token = myModelResults?.observe { [weak self] _ in
            self?.cellModels = self?.myModelResults?.map { ContentViewCellModel(id: $0.id, title: $0.title) } ?? []
        }
    }
    
    deinit {
        token?.invalidate()
    }
}

Viewもこれに合わせて変更します。

struct ContentView: View {
    @ObservedObject var model = ContentViewModel()
    
    var body: some View {
        NavigationView {
            List {
                ForEach(model.cellModels, id: \.id) { cellModel in
                    Button(action: {
                        let myModel = try? Realm().objects(MyModel.self).filter("id = %@", cellModel.id).first
                        try? Realm().write {
                            myModel?.title = "\(Date())"
                        }
                    }) {
                        Text("\(cellModel.title)")
                    }
                }.onDelete { indexSet in
                    let realm = try? Realm()
                    if let index = indexSet.first, let myModel = realm?.objects(MyModel.self).filter("id = %@", self.model.cellModels[index].id).first {
                        try? realm?.write {
                            realm?.delete(myModel)
                        }
                    }
                }
            }.navigationBarItems(trailing: Button(action: {
                let myModel = MyModel()
                myModel.title = "\(Date())"
                let realm = try? Realm()
                try? realm?.write {
                    realm?.add(myModel)
                }
            }) {
                Text("Add")
            })
        }
    }
}

これで削除しても落ちなくなりました。

f:id:llcc:20200426204652p:plain

Swiftでframeworkを作成する

アプリ開発で良く出てくる.frameworkという拡張子のライブラリを自作してみました。

frameworkを作成する

Xcodeで新規プロジェクト作成時にFrameworkを選択します。

f:id:llcc:20200412163759p:plain

プロジェクトは下のようなファイル構成になっています。

f:id:llcc:20200412164450p:plain

Productsの中に入っているMyFramework.frameworkの構成は下の通りです。

f:id:llcc:20200412164630p:plain

続けてライブラリにファイルを追加してみます。
追加したのは下のようにメソッドが一つだけあるクラスです。

別プロジェクトから呼び出すためにアクセス修飾子はpublicにしています。

import UIKit

public class MyClass {
    static public func test() {
        print("MyFramework")
    }
}

ビルドするとMyFramework.frameworkも更新されます。
Finderで見るとSwift用のヘッダーが作られている事が分かります。

f:id:llcc:20200412164908p:plain

frameworkを使ってみる

まずはプロジェクトにライブラリを追加します。
追加はXcodeFrameworks, Libraries, and Embedded Contentにframeworkファイルをドラッグするだけです。

f:id:llcc:20200412165056p:plain

以下のように書くことでMyClassのメソッドを使う事ができます。

import UIKit
import MyFramework

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        
        MyClass.test()
    }
}

Frameworkがプロジェクト外に配置されている場合、Framework Search Pathsにライブラリのパスを追加する必要があります。

f:id:llcc:20200412165840p:plain

frameworkのユニバーサル対応

上記の方法では、シミュレータか実機のどちらかでだけ動くライブラリのみ作れます。
これを両方の環境で動くように修正してみようと思います。

修正は下URLを参考にしました。

Swiftでフレームワークを作成する(第1回) | GMOアドパートナーズグループ TECH BLOG byGMO

まずは両対応ライブラリをつくためのターゲットを作成します。
XcodeのFile → New → Targetからターゲットを選んで、その中のAggregateを選びます。

f:id:llcc:20200412175631p:plain

AggregateにRun scriptを追加して下コードを貼り付けます。

#!/bin/sh
 
UNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-universal
 
# make sure the output directory exists
mkdir -p "${UNIVERSAL_OUTPUTFOLDER}"
 
# Step 1. Build Device and Simulator versions
xcodebuild -target "${PROJECT_NAME}" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphoneos  BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build
xcodebuild -target "${PROJECT_NAME}" -configuration ${CONFIGURATION} -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build
 
# Step 2. Copy the framework structure (from iphoneos build) to the universal folder
cp -R "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${PROJECT_NAME}.framework" "${UNIVERSAL_OUTPUTFOLDER}/"
 
# Step 3. Copy Swift modules from iphonesimulator build (if it exists) to the copied framework directory
SIMULATOR_SWIFT_MODULES_DIR="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.framework/Modules/${PROJECT_NAME}.swiftmodule/."
if [ -d "${SIMULATOR_SWIFT_MODULES_DIR}" ]; then
cp -R "${SIMULATOR_SWIFT_MODULES_DIR}" "${UNIVERSAL_OUTPUTFOLDER}/${PROJECT_NAME}.framework/Modules/${PROJECT_NAME}.swiftmodule"
fi
 
# Step 4. Create universal binary file using lipo and place the combined executable in the copied framework directory
lipo -create -output "${UNIVERSAL_OUTPUTFOLDER}/${PROJECT_NAME}.framework/${PROJECT_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.framework/${PROJECT_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${PROJECT_NAME}.framework/${PROJECT_NAME}"
 
# Step 5. Convenience step to copy the framework to the project's directory
cp -R "${UNIVERSAL_OUTPUTFOLDER}/${PROJECT_NAME}.framework" "${PROJECT_DIR}"
 
# Step 6. Convenience step to open the project's directory in Finder
open "${PROJECT_DIR}"

あとはAggregateを実行するだけで両対応フレームワークを作る事ができます。

続けて上記スクリプトの内容を見ていきます。
下ではユニバーサルフレームワーク用のディレクトリの作成をしています。

#!/bin/sh
 
UNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-universal
 
# make sure the output directory exists
mkdir -p "${UNIVERSAL_OUTPUTFOLDER}"

以下ではシミュレータ/実機それぞれ向けのフレームワークを作っています。
xcodebuildはXcodeに付いているコマンドラインツールで、プロジェクトのビルドをする事ができます。

# Step 1. Build Device and Simulator versions
xcodebuild -target "${PROJECT_NAME}" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphoneos  BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build
xcodebuild -target "${PROJECT_NAME}" -configuration ${CONFIGURATION} -sdk iphonesimulator ONLY_ACTIVE_ARCH=NO BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build

以下では生成したフレームワークやモジュールファイルのコピーをしています。

# Step 2. Copy the framework structure (from iphoneos build) to the universal folder
cp -R "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${PROJECT_NAME}.framework" "${UNIVERSAL_OUTPUTFOLDER}/"
 
# Step 3. Copy Swift modules from iphonesimulator build (if it exists) to the copied framework directory
SIMULATOR_SWIFT_MODULES_DIR="${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.framework/Modules/${PROJECT_NAME}.swiftmodule/."
if [ -d "${SIMULATOR_SWIFT_MODULES_DIR}" ]; then
cp -R "${SIMULATOR_SWIFT_MODULES_DIR}" "${UNIVERSAL_OUTPUTFOLDER}/${PROJECT_NAME}.framework/Modules/${PROJECT_NAME}.swiftmodule"
fi

以下ではlipoというツールを使ってユニバーサルフレームワークを作っています。
lipoはXcodeに付属しているツールで、シミュレータ/実機向けのフレームワークを結合する事ができます。

# Step 4. Create universal binary file using lipo and place the combined executable in the copied framework directory
lipo -create -output "${UNIVERSAL_OUTPUTFOLDER}/${PROJECT_NAME}.framework/${PROJECT_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.framework/${PROJECT_NAME}" "${BUILD_DIR}/${CONFIGURATION}-iphoneos/${PROJECT_NAME}.framework/${PROJECT_NAME}"

最後にプロジェクトのルートディレクトリにフレームワークをコピーしています。

# Step 5. Convenience step to copy the framework to the project's directory
cp -R "${UNIVERSAL_OUTPUTFOLDER}/${PROJECT_NAME}.framework" "${PROJECT_DIR}"
 
# Step 6. Convenience step to open the project's directory in Finder
open "${PROJECT_DIR}"

実際にUniversal対応になったかどうかはfileコマンドで確認する事ができます。

file MyFramework.framework/MyFramework

fileコマンドを実行すると下のように両環境に対応できている表示が出てきます。

MyFramework.framework/MyFramework: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit dynamically linked shared library x86_64] [arm64:Mach-O 64-bit dynamically linked shared library arm64]
MyFramework.framework/MyFramework (for architecture x86_64):    Mach-O 64-bit dynamically linked shared library x86_64
MyFramework.framework/MyFramework (for architecture arm64): Mach-O 64-bit dynamically linked shared library arm64

iOSシミュレータ用と実機用のバイナリの場合は下のような表示になります。

# iOSシミュレータ
MyFramework.framework/MyFramework: Mach-O 64-bit dynamically linked shared library x86_64
# 実機
MyFramework.framework/MyFramework: Mach-O 64-bit dynamically linked shared library arm64