Vlastní Dynamický Framework pro iOS, macOS, watchOS a tvOS

21. 12. 2023

Na Internetu lze najít spoustu návodů na sdílený kód mezi systémy pomocí Dynamických Frameworků, ale některé už nefungují, nebo jsou zbytečně složité. Tak jsem se rozhodl sepsat tutoriál a natočit video, které bude co nejkratší a shrne to vše do jednoho.

Hledáš jenom základní iOS Kuchařku? Podívejte se sem: 18+ věcí, které by měl dobrý iOS vývojář zvládat.

Jak už bylo rečeno, když chceme sdílet kód mezi iOS, tvOS, watchOS nebo macOS, tak nejjednodušší způsob je využít Dynamický Framework, který je na iOS dostupný od iOS 8.
Tutoriál lze rozdělit na tři části:

  1. Vytvoření frameworku obsahujícího náš sdílený kód.
  2. Nakonfigurování frameworku pro zařízení a simulátory.
  3. Použítí frameworku v iOS a macOS.

Pro ty, co jsou líní číst, jsem připravil video tutoriál:

Vytvoření Dynamického Frameworku

Otevřeme Xcode a vytvoříme nový projekt. File -> New -> Project. Zvolíme iOS -> Framework & Library -> Cocoa Touch Framework a pojmenujeme ho MyKit.

Untitled-1

Po vytvoření projektu přejmenujeme vytvořený target v projektu z MyKit na iOS MyKit a taktéž přejmenujeme vytvořené schéma pro daný target. Schemes -> Manage Schemes

Přidáme nový target pro macOS platformu a pojmenujeme ho macOS MyKit.

Odstraníme složku s názvem macOS MyKit, která se vytvořila s novým targetem pro macOS.
Projekt bude vypadat nějak takhle:

Untitled-3

Pro každý target otevřeme Build Settings a vyhledáme Product Name a změníme hodnotu na MyKit. Tím docílíme toho, že pro každou platformu se náš framework bude jmenovat stejně.

Untitled-5

Následně pro každý target znova v Build Settings vyhledáme Info.plist File a hodnotu z targetu iOS MyKit překopírujeme do ostatních targetů. Tím zajístíme jeden Info.plist soubor pro všechny targety.

Untitled-6

Vytvoříme náš první zdrojový soubor File -> New -> File a vybereme iOS -> Source -> Swift File a pojmenujeme ho Model. Nezapomeneme zaškrtnout všechny targety.

Obsah souboru Model.swift:

import Foundation

public class Model {
    
    public let devices: [String]
    
    public init() {
        self.devices = ["iPhone", "iPad", "iPod"]
    }
    
}

Vytvoříme další soubor, tentokrát shell script. File -> New -> File a vybereme iOS -> Other -> Shell Script. Pojmenujeme ho UniversalFramework a vybereme target pouze iOS MyKit.

Untitled-13

Obsah souboru UniversalFramework.sh:

if [ "true" == ${ALREADYINVOKED:-false} ]
then
echo "RECURSION: Detected, stopping"
else
export ALREADYINVOKED="true"

UNIVERSAL_OUTPUTFOLDER=${BUILD_DIR}/${CONFIGURATION}-iosuniversal

# make sure the output directory exists
mkdir -p "${UNIVERSAL_OUTPUTFOLDER}"

# Step 1. Build Device and Simulator versions
xcodebuild -target "${TARGET_NAME}" ONLY_ACTIVE_ARCH=NO -configuration ${CONFIGURATION} -sdk iphoneos  BUILD_DIR="${BUILD_DIR}" BUILD_ROOT="${BUILD_ROOT}" clean build
xcodebuild -target "${TARGET_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}"

fi

Modifikoval jsem TENTO originální skript.

Nově vytvořený shell soubor je potřeba nastavit jako spustitelný. Otevřeme Terminal. Applications -> Utitlities -> Terminal. Otevřeme si soubor UniversalFramework.sh ve Finderu.

Napíšeme příkaz:

$ chmod u+x "path_to_file"

Untitled-7

Přejdeme zpět do Xcode a vybereme Projekt MyKit a target iOS MyKit a následně se přepneme do Build Phases.

Klikneme na tlačítko + a vybereme New Run Script Phase a pojmenujeme si to jako Universal Framework.

Vložíme shell příkaz:

${SRCROOT}/UniversalFramework.sh

Untitled-8

Vybereme iOS MyKit schéma a libovolný simulátor zařízení.

Spustíme build vybraného schématu. Product -> Build

Přepneme schéma na macOS MyKit a spustíme build.

V levém sloupci vybereme složku Products a klikneme pravým tlačítkem na libovolný MyKit.framework a zvolíme Show in Finder.

Po otevření vidíme složky Debug-XYZ. Pro nás je duležitá složka Debug, která obsahuje MyKit framework pro macOS a složka Debug-iosuniversal, která obsahuje MyKit framework pro iOS a Simulator.

Tím je náš framework hotový a připraven k použití 🙂

Pojdme se teď podívát na to, jak ho použít v iOS a macOS aplikaci. Na iOS aplikaci bude potřeba dodělat odstranění x86/64 architektury z frameworku při nahrávání do iTunes.

Použití frameworku

Vytvoříme nový Workspace. File -> New -> Workspace. A pojmenujeme ho MyApp. Doporučuji si vytvořit stejnojmennou složku a do té vložit workspace soubor.

V levém dolním rohu stiskneme tlačítko + a zvolíme New Project.

Vybereme iOS -> Application -> Single View Application a pojmenujeme to jako MyApp. Doporučuji vytvořit složku iOS a do ní umístit projekt.

Untitled-15

Untitled-9

Untitled-14

Znovu vytvoříme nový projekt ale tentokrát macOS aplikaci. OS X -> Application -> Cocoa Application a pojmenujeme stéjně MyApp. Zase doporučuji vytvořit složku macOS a do ní vložit projekt.

Untitled-16

Ve workspace vybereme iOS aplikaci a klikneme na záložku General. Ze složky Debug-iosuniversal, kterou jsme otevřeli na konci vytváření frameworku, přetáhneme MyKit.framework do Embedded Binaries. Ujistíme se, že check box Copy Items If needed je odškrtnutý.

Stejnou akci provedeme pro macOS aplikaci. Zde akorát přetáhneme MyKit.framework ze složky Debugvb.

Untitled-11

Jak pro iOS, tak pro macOS aplikaci v záložce Build Settings vyhledáme Framework Search Paths a přidáme cestu k frameworku. Pro iOS to bude cesta ke složce Debug-iosuniversal a pro macOS složka Debug.

Nastal čas použít náš framework v aplikacích. Vybereme soubor ViewController.swift v iOS aplikaci a nahradíme ho tímhle:

import UIKit
import MyKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let model = Model()
        print(model.devices)
        
        // Do any additional setup after loading the view, typically from a nib.
    }

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

}

Stejnou akci provedeme pro macOS aplikaci. Takže soubor ViewController.swift v macOS aplikaci nahradíme timhle:

import Cocoa
import MyKit

class ViewController: NSViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let model = Model()
        print(model.devices)

        // Do any additional setup after loading the view.
    }

    override var representedObject: AnyObject? {
        didSet {
        // Update the view, if already loaded.
        }
    }
}

Pro iOS aplikaci vytvoříme nový shell script. File -> New -> File a potom iOS -> Other -> Shell Script a pojmenujeme ho Trim.sh

 

Obsah souboru Trim.sh:

FRAMEWORK=$1
echo "Trimming $FRAMEWORK..."

FRAMEWORK_EXECUTABLE_PATH="${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/$FRAMEWORK.framework/$FRAMEWORK"

EXTRACTED_ARCHS=()

for ARCH in $ARCHS
do
echo "Extracting $ARCH..."
lipo -extract "$ARCH" "$FRAMEWORK_EXECUTABLE_PATH" -o "$FRAMEWORK_EXECUTABLE_PATH-$ARCH"
EXTRACTED_ARCHS+=("$FRAMEWORK_EXECUTABLE_PATH-$ARCH")
done

echo "Merging binaries..."
lipo -o "$FRAMEWORK_EXECUTABLE_PATH-merged" -create "${EXTRACTED_ARCHS[@]}"
rm "${EXTRACTED_ARCHS[@]}"
rm "$FRAMEWORK_EXECUTABLE_PATH"
mv "$FRAMEWORK_EXECUTABLE_PATH-merged" "$FRAMEWORK_EXECUTABLE_PATH"

echo "Done."

Zdroj ZDE

Opět přes terminál nastavíme soubor jako spustitelný.

$chmod u+x "cesta_k_souboru"

V záložce Build Phases klikneme na tlačítko + a vybereme New Run Script Phase a pojmenujeme si to jako Trim Framework.

Vložíme shell příkaz:

${SRCROOT}/Trim.sh MyKit

Untitled-12

Vybereme schéma pro iOS aplikaci a spustíme bulid. Product -> Build.

Vybereme schéma pro macOS aplikaci a spustíme build.

A máme hotovo!

Vývoj mobilní aplikace: 11) Funguje mi vůbec ta aplikace? A co v ní dělají uživatelé?

Vývoj mobilní aplikace má mnoho různorodých aspektů, které je dobré vědět. V následujícím seriálu Vám představujeme jednotlivé díly, které Vás …

Číst článek

Vývoj mobilní aplikace: 10) Jak na soukromí v aplikaci, nebo jak zvládnout schvalování na storech

Vývoj mobilní aplikace má mnoho různorodých aspektů, které je dobré vědět. V následujícím seriálu Vám představujeme jednotlivé díly, které Vás …

Číst článek

Vývoj mobilní aplikace: 9) Jak na Google Play a AppStore a jestli vůbec

Vývoj mobilní aplikace má mnoho různorodých aspektů, které je dobré vědět. V následujícím seriálu Vám představujeme jednotlivé díly, které Vás …

Číst článek

Kontakt