UE Development Notes: Mac/iOS

UE开发笔记:Mac/iOS篇

The main content of this article introduces the deployment of the UE development environment on Mac, configuration of iOS remote packaging, the application of UPL on iOS (intervening in the ipa packaging process), tools and development skills, as well as analysis of related engine code, documenting some pitfalls encountered in the project. It is primarily compiled from my previous notes, and related Mac and iOS content will also be updated in this article in the future.

Remote Build for iOS

UE can directly package Android and Windows builds on Windows (Android requires configuring JDK/SDK/NDK/Gradle and other environments). Packaging iOS requires a Mac. If every time packaging iOS you have to perform operations on a Mac, it is essentially the same as on Windows, but the process cannot be reused, making it cumbersome, especially when the engine is modified. You need to first update the engine code and compile it, then update the project and execute packaging. Fortunately, UE provides remote iOS build capabilities, allowing packaging for Win/Android/iOS to be done in the same build process.

Pre-requisites:

  1. One computer running MacOS on the intranet (white or black)
  2. Apply for a p12 certificate and mobileprovision
  3. The PC and Mac need to be able to access each other on the network (in the same subnet)

IOS Certificate Application

Before packaging iOS, you need to apply for an iOS developer account to create certificates, which can be applied at developer.apple.com. You need to obtain the p12 certificate and mobileprovision, which should then be imported into UE’s project settings. Note that you must match whether the created certificate is a Developer or Distribution certificate during packaging; otherwise, packaging will fail.

There are many articles on the internet detailing the certificate application process. Here, I will simply record the process I followed, and the steps are not the most detailed, only for reference.

Firstly, export a certificate on the Mac:
Open the software Keychain Access - Certificate Assistant - Request a Certificate from a Certificate Authority:

Choose to save it to disk, which generates a file named CertificateSigningRequest.certSigningRequest.
Then log in to Apple Developer, go to Account - Certificates:

After entering, create Apple Development or iOS App Development, and during the creation process, upload the CertificateSigningRequest.certSigningRequest file generated above.

Add devices:

You can use UE’s IPhonePackager.exe to check the uuid of iOS devices:

Generate Provision:

After generating it, download the provision file:

In Apple Developer, only the cer certificate can be downloaded, but the engine requires a p12. You need to import the cer certificate into the Mac’s Keychain, then export it as a p12 certificate:

Note: Make sure to select under the “Certificates” category; otherwise, the p12 option will be grayed out.

Configuration for Remote Build

Packaging iOS with UE requires importing the certificate and provision in the project settings, and setting the BundleName and Bundle Identifier to the Bundle ID configured on the Apple Developer website, formatted as com.xxxxx.yyyyyy.

After importing the p12 certificate and mobileprovision, it should appear as in the figure:

Once the certificate is imported, you can start configuring remote packaging.

First, enable remote login in Mac’s System Preferences - Sharing:

Then on Windows, add the mobileprovision to the project and set the BundleName and Bundle Identifier.
Next, scroll down to find IOS - Build under Remote Build Options:

Enter the IP address of the target Mac (if no port is specified, the default is 22; if specifying a port, use the format xx.xx.xx.xx:2222 with a colon separating them) and username.

Then click Generated SSH Key, and a window will pop up:

Press any key to continue.
You will be prompted to enter a password; enter it as instructed. Then you will be asked to enter the password for the MAC computer, and after entering, it will prompt:

1
Enter passphrase (empty for no passphrase):

This asks you to input the password for the generated ssh Key, which you can normally skip by just pressing Enter.
Pressing Enter continuously will inform you that the ssh key has been successfully generated:

Then you will be asked to enter the first set password and the password for the target MAC machine, and after completion, it will indicate there are no errors, which is good:

The path where the generated SSH Key is stored is:

1
C:\Users\imzlp\AppData\Roaming/Unreal Engine/UnrealBuildTool/SSHKeys/192.168.2.89/imzlp/RemoteToolChainPrivate.key

If you want to share it with other members in the group, share the RemoteToolChainPrivate.key, and have them set IOS - Build - RemoteBuildOptions to use the Override existing SSH Permissions file path pointing to RemoteToolChainPrivate.key.

After this, you can package just like packaging for Windows or packaging iOS on Win:

Remote Mac packaging goes through several stages:

  1. Upload the engine and project code from your local machine to Mac
  2. Execute compilation on Mac
  3. After compilation is complete, generate the ipa package on Mac (but excluding Cooked resources)
  4. Pull back the generated ipa package to the local machine, unpack, Cook art resources, and merge into ipa

For the first step, uploading the local engine and project code to Mac is achieved using rsync; the Engine\Build\Rsync directory contains filters for files that need to be uploaded to the target machine during remote construction.
For projects, you can create the <ProjectDir>/Build/Rsync/RsyncProject.txt file in the project directory and add your desired file filtering for uploading to Mac, which can solve the problem of some files being overlooked during remote packaging.
The default RsyncProject.txt filter used during uploads by UE includes both engine and project directories; for the specific code, see: UnrealBuildTool/ToolChain/RemoteMac.cs#L927

SSH Key Login Failure

If you receive the following error when making the SSH connection:

1
lipengzha@192.168.31.55: Permission denied (publickey,password,keyboard-interactive).

You may modify the Mac’s SSH configuration (/etc/ssh/sshd_config):

1
2
RSAAuthentication yes
PubkeyAuthentication yes

Then reload the configuration:

1
2
sudo launchctl unload /System/Library/LaunchDaemons/ssh.plist
sudo launchctl load -w /System/Library/LaunchDaemons/ssh.plist

iOS Packaging Certificate Configuration Error Issue

When adding Provision and Certificate in UE’s project settings, the Bundle Identifier must correspond to the certificate; however, after setting up, if you try to package, you may receive the following error:

1
2
Provision not found. A provision is required for deploying your app to the device.  
Signing key not found. The app could not be digitally signed, because the signing key is not configured.

If such a situation arises after configuring the certificate and Provision, check whether the certificate is Development or Distribution. By default, the project setting does not check Distribution. If the imported certificate is a distribution certificate, it can only package as Shipping and must check Distribution.

If you use a distribution certificate without checking For Distribution, you will encounter the following error during packaging:

1
2
3
Check dependencies
Code Signing Error: Provisioning profile "com.tencent.tmgp.zyhx_Production_SignProvision" doesn't match the entitlements file's value for the get-task-allow entitlement.
Code Signing Error: Code signing is required for product type 'Application' in SDK 'iOS 13.6'

SSH Key Path Lookup Bug

Previously mentioned in Project Settings - Platforms - iOS, you can specify the SSH Key in Override Existing SSH Permissions file; if not specified, it defaults to using the engine lookup path. By default, it searches for the following paths:

1
2
3
4
5
6
7
8
9
10
const FString DefaultKeyFilename = TEXT("RemoteToolChainPrivate.key");
const FString RelativeFilePathLocation = FPaths::Combine(TEXT("SSHKeys"), *RemoteServerName, *RSyncUsername, *DefaultKeyFilename);
TArray<FString> PossibleKeyLocations;
PossibleKeyLocations.Add(FPaths::Combine(*FPaths::ProjectDir(), TEXT("Build"), TEXT("NotForLicensees"), *RelativeFilePathLocation));
PossibleKeyLocations.Add(FPaths::Combine(*FPaths::ProjectDir(), TEXT("Build"), TEXT("NoRedist"), *RelativeFilePathLocation));
PossibleKeyLocations.Add(FPaths::Combine(*FPaths::ProjectDir(), TEXT("Build"), *RelativeFilePathLocation));
PossibleKeyLocations.Add(FPaths::Combine(*FPaths::EngineDir(), TEXT("Build"), TEXT("NotForLicensees"), *RelativeFilePathLocation));
PossibleKeyLocations.Add(FPaths::Combine(*FPaths::EngineDir(), TEXT("Build"), TEXT("NoRedist"), *RelativeFilePathLocation));
PossibleKeyLocations.Add(FPaths::Combine(*FPaths::EngineDir(), TEXT("Build"), *RelativeFilePathLocation));
PossibleKeyLocations.Add(FPaths::Combine(*Path, TEXT("Unreal Engine"), TEXT("UnrealBuildTool"), *RelativeFilePathLocation));

However, when the RemoteServerName includes a port, there is a bug in UE’s implementation when hoping to use the engine lookup path.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void UIOSRuntimeSettings::PostInitProperties()
{
Super::PostInitProperties();

// We can have a look for potential keys
if (!RemoteServerName.IsEmpty() && !RSyncUsername.IsEmpty())
{
SSHPrivateKeyLocation = TEXT("");

const FString DefaultKeyFilename = TEXT("RemoteToolChainPrivate.key");
const FString RelativeFilePathLocation = FPaths::Combine(TEXT("SSHKeys"), *RemoteServerName, *RSyncUsername, *DefaultKeyFilename);

FString Path = FPlatformMisc::GetEnvironmentVariable(TEXT("APPDATA"));

TArray<FString> PossibleKeyLocations;
PossibleKeyLocations.Add(FPaths::Combine(*FPaths::ProjectDir(), TEXT("Build"), TEXT("NotForLicensees"), *RelativeFilePathLocation));
PossibleKeyLocations.Add(FPaths::Combine(*FPaths::ProjectDir(), TEXT("Build"), TEXT("NoRedist"), *RelativeFilePathLocation));
PossibleKeyLocations.Add(FPaths::Combine(*FPaths::ProjectDir(), TEXT("Build"), *RelativeFilePathLocation));
PossibleKeyLocations.Add(FPaths::Combine(*FPaths::EngineDir(), TEXT("Build"), TEXT("NotForLicensees"), *RelativeFilePathLocation));
PossibleKeyLocations.Add(FPaths::Combine(*FPaths::EngineDir(), TEXT("Build"), TEXT("NoRedist"), *RelativeFilePathLocation));
PossibleKeyLocations.Add(FPaths::Combine(*FPaths::EngineDir(), TEXT("Build"), *RelativeFilePathLocation));
PossibleKeyLocations.Add(FPaths::Combine(*Path, TEXT("Unreal Engine"), TEXT("UnrealBuildTool"), *RelativeFilePathLocation));

// Find a potential path that we will use if the user hasn't overridden.
// For information purposes only
for (const FString& NextLocation : PossibleKeyLocations)
{
if (IFileManager::Get().FileSize(*NextLocation) > 0)
{
SSHPrivateKeyLocation = NextLocation;
break;
}
}
}
// ...
}

This code has bugs on Windows because when RemoteServerName includes a specified port, the SSH Key cannot be found under Windows since paths on Windows cannot contain a colon. Therefore, there will be issues when looking for the Key path, which requires modifications in the engine to resolve.
Modify the above code:

1
2
3
4
5
6
7
8
9
SSHPrivateKeyLocation = TEXT("");
FString RealRemoteServerName = RemoteServerName;
if(RemoteServerName.Contains(TEXT(":")))
{
FString RemoteServerPort;
RemoteServerName.Split(TEXT(":"),&RealRemoteServerName,&RemoteServerPort);
}
const FString DefaultKeyFilename = TEXT("RemoteToolChainPrivate.key");
const FString RelativeFilePathLocation = FPaths::Combine(TEXT("SSHKeys"), *RealRemoteServerName, *RSyncUsername, *DefaultKeyFilename);

Recompiling the engine can resolve this.

Remote Shader Compilation Key Lookup Bug

Note: Since version 4.26 and later, the engine version supports compiling Metal shaders on Windows, details can be found in the documents:

In Project Settings - Platforms - IOS, enabling Enable Remote Shader Compile, if the build machine address includes a specified port, there will be issues in searching for the SSH Key:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Developer/Apple/MetalShaderFormat/Private/MetalShaderCompiler.cpp
bool IsRemoteBuildingConfigured(const FShaderCompilerEnvironment* InEnvironment)
{
// ...
GRemoteBuildServerSSHKey = "";
if (InEnvironment != nullptr && InEnvironment->RemoteServerData.Contains(TEXT("SSHPrivateKeyOverridePath")))
{
GRemoteBuildServerSSHKey = InEnvironment->RemoteServerData[TEXT("SSHPrivateKeyOverridePath")];
}
if (GRemoteBuildServerSSHKey.Len() == 0)
{
GConfig->GetString(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings"), TEXT("SSHPrivateKeyOverridePath"), GRemoteBuildServerSSHKey, GEngineIni);

GConfig->GetString(TEXT("/Script/IOSRuntimeSettings.IOSRuntimeSettings"), TEXT("SSHPrivateKeyOverridePath"), GRemoteBuildServerSSHKey, GEngineIni);
if (GRemoteBuildServerSSHKey.Len() == 0)
{
if (!FParse::Value(FCommandLine::Get(), TEXT("serverkey"), GRemoteBuildServerSSHKey) && GRemoteBuildServerSSHKey.Len() == 0)
{
if (GRemoteBuildServerSSHKey.Len() == 0)
{
// RemoteToolChain.cs in UBT looks in a few more places but the code in FIOSTargetSettingsCustomization::OnGenerateSSHKey() only puts the key in this location so just going with that to keep things simple
FString Path = FPlatformMisc::GetEnvironmentVariable(TEXT("APPDATA"));
GRemoteBuildServerSSHKey = FString::Printf(TEXT("%s\\Unreal Engine\\UnrealBuildTool\\SSHKeys\\%s\\%s\\RemoteToolChainPrivate.key"), *Path, *GRemoteBuildServerHost, *GRemoteBuildServerUser);
}
}
}
}
// ...
}

As you can see, the Key path is directly concatenated using GRemoteBuildServerHost, but if a port is specified in the configuration, the value of GRemoteBuildServerHost will be in the format xxx.xx.xx.xx:1234. However, on Windows, directory names cannot contain :, resulting in failure to locate the Key.

In the same file, in the ExecRemoteProcess function, there is no processing for cases involving a specified port:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool ExecRemoteProcess(const TCHAR* Command, const TCHAR* Params, int32* OutReturnCode, FString* OutStdOut, FString* OutStdErr)
{
#if PLATFORM_MAC && !UNIXLIKE_TO_MAC_REMOTE_BUILDING
return FPlatformProcess::ExecProcess(Command, Params, OutReturnCode, OutStdOut, OutStdErr);
#else
if (GRemoteBuildServerHost.IsEmpty())
{
return false;
}
FString CmdLine = FString(TEXT("-i \"")) + GRemoteBuildServerSSHKey + TEXT("\" \"") + GRemoteBuildServerUser + '@' + GRemoteBuildServerHost + TEXT("\" ") + Command + TEXT(" ") + (Params != nullptr ? Params : TEXT(""));
return ExecProcess(*GSSHPath, *CmdLine, OutReturnCode, OutStdOut, OutStdErr);

#endif
}

This needs to be handled:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool ExecRemoteProcess(const TCHAR* Command, const TCHAR* Params, int32* OutReturnCode, FString* OutStdOut, FString* OutStdErr)
{
#if PLATFORM_MAC && !UNIXLIKE_TO_MAC_REMOTE_BUILDING
return FPlatformProcess::ExecProcess(Command, Params, OutReturnCode, OutStdOut, OutStdErr);
#else
if (GRemoteBuildServerHost.IsEmpty())
{
return false;
}

FString RemoteBuildServerIP = GRemoteBuildServerHost;
FString RemoteBuildServerPort = TEXT("22");

if(GRemoteBuildServerHost.Contains(TEXT(":")))
{
GRemoteBuildServerHost.Split(TEXT(":"),&RemoteBuildServerIP,&RemoteBuildServerPort);
}

FString CmdLine = FString(TEXT("-i \"")) + GRemoteBuildServerSSHKey + TEXT("\" \"") + GRemoteBuildServerUser + '@' + RemoteBuildServerIP + TEXT("\" ") TEXT("-p ") + RemoteBuildServerPort +TEXT(" ")+ Command + TEXT(" ") + (Params != nullptr ? Params : TEXT(""));
return ExecProcess(*GSSHPath, *CmdLine, OutReturnCode, OutStdOut, OutStdErr);

#endif
}

Signing Errors

Developer Signature Expired

If you see the following logs during packaging, it indicates that the iOS certificate has expired:

1
2
3
/Users/buildmachine/UE4/Builds/lipengzha-PCb/C/BuildAgent/workspace/FGameEngine/Client/Intermediate/ProjectFilesIOS/FGame.xcodeproj: error: No certificate for team '6H9S4KQ3C9' matching 'iPhone Developer: Created via API (7ZQD4FUW73)' found: Select a different signing certificate for CODE_SIGN_IDENTITY, a team that matches your selected certificate, or switch to automatic provisioning. (in target 'FGame' from project 'FGame')
warning: Run script build phase 'Sign Manual Frameworks' will be run during every build because it does not specify any outputs. To address this warning, either add output dependencies to the script phase, or configure it to run in every build by unchecking "Based on dependency analysis" in the script phase. (in target 'FGame')
** BUILD FAILED **

Simply updating the certificate will resolve the issue.

Apple Intermediate Certificate Expired

If the following error appears when packaging on Windows:

1
2
IPP ERROR: Application exception: System.Security.Cryptography.CryptographicException
A certificate chain could not be built to a trusted root authority.

This is also caused by the expiration of an Apple intermediate certificate; updating it will suffice. See: BuildGraph: Building a binary engine supporting multi-platform packaging

Notes

When conducting remote iOS builds on Windows, several constraints apply:

  1. The individual file size within the IPA cannot exceed 2G, such as Pak files.
  2. The total size of the IPA cannot exceed 4G.

Otherwise, you may encounter the following error:

1
2
3
4
5
6
7
8
9
IPP ERROR: Application exception: Ionic.Zip.ZipException: Compressed or Uncompressed size, or offset exceeds the maximum value. Consider setting the UseZip64WhenSaving property on the ZipFile instance.
Ionic.Zip.ZipEntry.SetZip64Flags()
Ionic.Zip.ZipEntry.PostProcessOutput(Stream s)
Ionic.Zip.ZipEntry._WriteEntryData(Stream s)
Ionic.Zip.ZipEntry.Write(Stream s)
Ionic.Zip.ZipFile.Save()
iPhonePackager.FileOperations.ZipFileSystem.Close()
iPhonePackager.CookTime.RepackageIPAFromStub()
iPhonePackager.Program.Main(String[] args)

Mac Development Environment

Installing UE on MacOS

UE requires the partition format of MacOS to be case-insensitive (otherwise, EpicLauncher cannot install), and the engine installation requires the system version to be greater than 10.13.5; otherwise, there may be crashes and unsupported situations (attempting to ignore errors may not result in successful installation).
Compiling relies on Xcode, similar to how it depends on VS, necessitating the installation of the compilation environment.
If an error message appears while creating a project after installing UE and Xcode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
An error occurred while trying to generate project files.
Running Mono...
Setting up Mono
/Users/Shared/UnrealEngine/4.22/Engine /Users/Shared/UnrealEngine/4.22/Engine/Binaries/Mac
Discovering modules, targets and source code for project...
Compiling with non-standard Xcode (xcode-select): /Library/Developer/CommandLineTools/
Triggered an exception while looking for SDK directory in Xcode.app
System.IO.DirectoryNotFoundException: Directory '/Library/Developer/CommandLineTools/Platforms/MacOSX.platform/Developer/SDKs' not found.
at System.IO.Directory.ValidateDirectoryListing (System.String path, System.String searchPattern, System.Boolean& stop) [0x00000] in <filename unknown>:0
at System.IO.Directory.GetFileSystemEntries (System.String path, System.String searchPattern) [0x00000] in <filename unknown>:0
at System.IO.Directory.GetDirectories (System.String path, System.String searchPattern) [0x00000] in <filename unknown>:0
at System.IO.Directory.GetDirectories (System.String path) [0x00000] in <filename unknown>:0
at UnrealBuildTool.AppleToolChain.SelectSDK (System.String BaseSDKDir, System.String OSPrefix, System.String& PlatformSDKVersion, Boolean bVerbose) [0x00000] in <filename unknown>:0
ERROR: Invalid SDK MacOSX.sdk, not found in /Library/Developer/CommandLineTools/Platforms/MacOSX.platform/Developer/SDKs

You should install Xcode Command Line Tools, and then execute the following command:

1
$ sudo ln -s /Applications/Xcode.app/Contents/Developer/Platforms /Library/Developer/CommandLineTools/

Modify SSHD Default Port on Mac

Sometimes internal networks have port restrictions, and ports below xxxx are not typically open, so how to conduct remote builds when port 22 is restricted? Modify the default port for SSH!

Edit the ssh port in the /etc/services file:

1
$ vim /etc/services

Change the SSH port to another value:

1
2
ssh              22/udp     # SSH Remote Login Protocol
ssh 22/tcp # SSH Remote Login Protocol

Change to:

1
2
ssh              2222/udp     # SSH Remote Login Protocol
ssh 2222/tcp # SSH Remote Login Protocol

Save and exit. You will also need to reload the configuration to make the new port effective:

1
2
sudo launchctl unload /System/Library/LaunchDaemons/ssh.plist
sudo launchctl load -w /System/Library/LaunchDaemons/ssh.plist

Then test if the port is connectable:

1
ssh localhost -p 2222

Enable Multi-threading Compilation for Xcode

First, check the Mac’s hardware configuration:

1
sysctl machdep.cpu

Find the field machdep.cpu.core_count, where the value is the number of cores on the Mac.

Then you can enable multi-threading for Xcode, with the number being cores * 2; for instance, if you have 8 cores, you can enable 16 threads:

1
defaults write com.apple.Xcode PBXNumberOfParallelBuildSubtasks 16

Built-in Frameworks in Mac System

Sometimes it is necessary to introduce system frameworks in UE modules. What are the default frameworks included in Mac? You can check in the following way. I am using system version 10.15.2, which can be checked with sw_vers:

1
2
3
4
buildmachine@LIPENGZHA-MC0 ~ % sw_vers
ProductName: Mac OS X
ProductVersion: 10.15.2
BuildVersion: 19C57

The built-in frameworks are located in the following directory:

1
/System/Library/Frameworks

The 10.15.2 version includes the following frameworks:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
AGL.framework                           ColorSync.framework                     CoreVideo.framework                    GameKit.framework
LinkPresentation.framework OpenGL.framework SoundAnalysis.framework
AVFoundation.framework Combine.framework CoreWLAN.framework GameplayKit.framework
LocalAuthentication.framework PCSC.framework Speech.framework
AVKit.framework Contacts.framework CryptoKit.framework HIDDriverKit.framework
MapKit.framework PDFKit.framework SpriteKit.framework
Accelerate.framework ContactsUI.framework CryptoTokenKit.framework Hypervisor.framework
MediaAccessibility.framework PencilKit.framework StoreKit.framework
Accounts.framework CoreAudio.framework DVDPlayback.framework ICADevices.framework
MediaLibrary.framework Photos.framework SwiftUI.framework
AdSupport.framework CoreAudioKit.framework DeviceCheck.framework IMServicePlugIn.framework
MediaPlayer.framework PhotosUI.framework SyncServices.framework
AddressBook.framework CoreAudioTypes.framework DirectoryService.framework IOBluetooth.framework
MediaToolbox.framework PreferencePanes.framework System.framework
AppKit.framework CoreBluetooth.framework DiscRecording.framework IOBluetoothUI.framework
Message.framework PushKit.framework SystemConfiguration.framework
AppleScriptKit.framework CoreData.framework DiscRecordingUI.framework IOKit.framework
Metal.framework Python.framework SystemExtensions.framework
AppleScriptObjC.framework CoreDisplay.framework DiskArbitration.framework IOSurface.framework
MetalKit.framework QTKit.framework TWAIN.framework
ApplicationServices.framework CoreFoundation.framework DriverKit.framework IOUSBHost.framework
MetalPerformanceShaders.framework Quartz.framework Tcl.framework
AudioToolbox.framework CoreGraphics.framework EventKit.framework IdentityLookup.framework
MetricKit.framework QuartzCore.framework Tk.framework
AudioUnit.framework CoreHaptics.framework ExceptionHandling.framework ImageCaptureCore.framework
ModelIO.framework QuickLook.framework USBDriverKit.framework
AudioVideoBridging.framework CoreImage.framework ExecutionPolicy.framework ImageIO.framework
MultipeerConnectivity.framework QuickLookThumbnailing.framework UserNotifications.framework
AuthenticationServices.framework CoreLocation.framework ExternalAccessory.framework InputMethodKit.framework
NaturalLanguage.framework RealityKit.framework VideoDecodeAcceleration.framework
Automator.framework CoreMIDI.framework FWAUserLib.framework InstallerPlugins.framework
NetFS.framework Ruby.framework VideoSubscriberAccount.framework
BackgroundTasks.framework CoreMIDIServer.framework FileProvider.framework InstantMessage.framework
Network.framework SafariServices.framework VideoToolbox.framework
BusinessChat.framework CoreML.framework FileProviderUI.framework Intents.framework
NetworkExtension.framework SceneKit.framework Vision.framework
CFNetwork.framework CoreMedia.framework FinderSync.framework JavaFrameEmbedding.framework
NetworkingDriverKit.framework ScreenSaver.framework WebKit.framework
CalendarStore.framework CoreMediaIO.framework ForceFeedback.framework JavaScriptCore.framework
NotificationCenter.framework ScriptingBridge.framework iTunesLibrary.framework
CallKit.framework CoreMotion.framework Foundation.framework JavaVM.framework
OSAKit.framework Security.framework vecLib.framework
Carbon.framework CoreServices.framework GLKit.framework Kerberos.framework
OSLog.framework SecurityFoundation.framework vmnet.framework
CloudKit.framework CoreSpotlight.framework GLUT.framework Kernel.framework
OpenAL.framework SecurityInterface.framework
Cocoa.framework CoreTelephony.framework GSS.framework LDAP.framework
OpenCL.framework ServiceManagement.framework
Collaboration.framework CoreText.framework GameController.framework LatentSemanticMapping.framework
OpenDirectory.framework Social.framework
```### Installing Command Line Tools

> Xcode's metal shader compiler was not found, verify Xcode has been installed on this Mac and that it has been selected in Xcode > Preferences > Locations > Command-line Tools.

Related issues:

- [Unreal Engine inaccurately reports "XCode is too old" when CommandLineTools installed](https://answers.unrealengine.com/questions/723143/unreal-engine-inaccurately-reports-xcode-is-too-ol-1.html)

To install XCode and Command Line Tools for Xcode offline, you can download from Apple's developer website: [More Downloads for Apple Developers](https://developer.apple.com/download/more/)

After installing `Command Line Tools`, if the error still appears during cooking, you need to execute the following command (first ensure that the `/Library/Developer/CommandLineTools` path exists; generally, this is the default installation path for `Command Line Tools`):

```bash
$ sudo xcode-select -s /Library/Developer/CommandLineTools

After setting CommandLineTools, you may encounter a message during packaging:

1
ERROR: Invalid SDK MacOSX.sdk, not found in /Library/Developer/CommandLineTools/Platforms/MacOSX.platform/Developer/SDKs

This is because, after setting it as CommandLineTools via xcode-select, the libraries in Xcode are no longer found during packaging. The solution is to create a symbolic link for the Platforms directory from Xcode in the CommandLineTools directory:

1
sudo ln -s /Applications/Xcode.app/Contents/Developer/Platforms /Library/Developer/CommandLineTools/Platforms

actool Error

1
2
UATHelper: Packaging (iOS):   xcrun: error: unable to find utility "actool", not a developer tool or in PATH
PackagingResults: Error: unable to find utility "actool", not a developer tool or in PATH

This error occurs because, after setting CommandLineTool as the default command line tool, the CommandLinTool/use/bin directory does not contain tools like actool. This is a frustrating problem; using Xcode as the default command line tool leads to cooking failures, while using CommandLineTool creates issues during compilation. My workaround is to link /Applications/Xcode.app/Contents/Developer/usr/bin to /Library/Developer/CommandLineTools/usr:

1
2
3
4
# Of course, back up CommandLineTool/usr/bin first
$ mv /Library/Developer/CommandLineTools/usr/bin /Library/Developer/CommandLineTools/usr/Command_bin
# Create symbolic link for Xcode's bin directory
$ sudo ln -s /Applications/Xcode.app/Contents/Developer/usr/bin /Library/Developer/CommandLineTools/usr/bin

Remove Restrictions on macOS Installed Software

  • Allow any source, run third-party applications
1
sudo spctl --master-disable
  • Install grayed-out dmg
1
hdiutil attach #dmg filename#

macOS Read/Write NTFS

After installing macOS, I found that macOS could read NTFS files but could not write to them, which is quite frustrating. It was said that this is due to Microsoft’s restrictions, but macOS itself has the read/write capability, which is just disabled. You can enable it via the following method.

First, execute diskutil list in the terminal to view disk information:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
visionsmile$ diskutil list
/dev/disk0 (internal, physical):
#: TYPE NAME SIZE IDENTIFIER
0: GUID_partition_scheme *240.1 GB disk0
1: EFI 312.5 MB disk0s1
2: Microsoft Reserved 134.2 MB disk0s2
3: Microsoft Basic Data Windows 164.3 GB disk0s3
4: Apple_APFS Container disk1 75.3 GB disk0s4

/dev/disk1 (synthesized):
#: TYPE NAME SIZE IDENTIFIER
0: APFS Container Scheme - +75.3 GB disk1
Physical Store disk0s4
1: APFS Volume OSX 34.2 GB disk1s1
2: APFS Volume Preboot 21.1 MB disk1s2
3: APFS Volume Recovery 509.8 MB disk1s3
4: APFS Volume VM 2.1 GB disk1s4

/dev/disk2 (external, physical):
#: TYPE NAME SIZE IDENTIFIER
0: GUID_partition_scheme *4.0 TB disk2
1: Microsoft Reserved 134.2 MB disk2s1
2: Microsoft Basic Data Document 4.0 TB disk2s2

/dev/disk3 (internal, physical):
#: TYPE NAME SIZE IDENTIFIER
0: FDisk_partition_scheme *1.0 TB disk3
1: Windows_NTFS Documents2 1.0 TB disk3s1

You need to note the NAME information of the NTFS disk; here I have three NTFS partitions: windows/Documents/Documents2.

Next, execute the command to update the fstab file:

1
sudo nano /etc/fstab

Enter the following content and replace what comes after LABEL= with the names of the partitions recorded above:

1
2
3
LABEL=Windows none ntfs rw,auto,nobrowse
LABEL=Documents none ntfs rw,auto,nobrowse
LABEL=Documents2 none ntfs rw,auto,nobrowse

After rebooting, you will be able to write to the NTFS partitions. Note: After rebooting, the removable disk will not display on the desktop; you must open Finder to see it.

Issues Encountered in UE4 Projects on Mac

Cook Errors

When encountering an error during cooking, the error message is:

1
CookResults: Error: Package Native Shader Library failed for MacNoEditor.

This is caused by the setting in Project Settings-Packing-Shared Material Native Library. Turning this off will resolve the issue.

PS: I saw a similar bug submission in UE’s issue tracker, but it was marked as fixed in 4.18: UE-49105. I don’t know why this issue still exists.

UPL Application in iOS

Unreal Plugin Language is a XML-based language provided by UE to control the process of building Apk and ipa, such as modifying AndroidManifest.xml or info.plist.

Analysis of plist Generation Process

In the UEDeployIOS.cs file, the GeneratePList function constructs the UPL object using the incoming UPLScripts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public virtual bool GeneratePList(FileReference ProjectFile, UnrealTargetConfiguration Config, string ProjectDirectory, bool bIsUE4Game, string GameName, bool bIsClient, string ProjectName, string InEngineDir, string AppDirectory, List<string> UPLScripts, VersionNumber SdkVersion, string BundleID, bool bBuildAsFramework, out bool bSupportsPortrait, out bool bSupportsLandscape, out bool bSkipIcons)
{
// remember name with -IOS-Shipping, etc
// string ExeName = GameName;

// strip out the markup
GameName = GameName.Split("-".ToCharArray())[0];

List<string> ProjectArches = new List<string>();
ProjectArches.Add("None");

string BundlePath;

// get the receipt
if (bIsUE4Game)
{
// ReceiptFilename = TargetReceipt.GetDefaultPath(UnrealBuildTool.EngineDirectory, "UE4Game", UnrealTargetPlatform.IOS, Config, "");
BundlePath = Path.Combine(UnrealBuildTool.EngineDirectory.ToString(), "Intermediate", "IOS-Deploy", "UE4Game", Config.ToString(), "Payload", "UE4Game.app");
}
else
{
// ReceiptFilename = TargetReceipt.GetDefaultPath(new DirectoryReference(ProjectDirectory), GameName, UnrealTargetPlatform.IOS, Config, "");
BundlePath = AppDirectory;//Path.Combine(ProjectDirectory, "Binaries", "IOS", "Payload", ProjectName + ".app");
}

string RelativeEnginePath = UnrealBuildTool.EngineDirectory.MakeRelativeTo(DirectoryReference.GetCurrentDirectory());

UnrealPluginLanguage UPL = new UnrealPluginLanguage(ProjectFile, UPLScripts, ProjectArches, "", "", UnrealTargetPlatform.IOS);

// Passing in true for distribution is not ideal here but given the way that ios packaging happens and this call chain it seems unavoidable for now, maybe there is a way to correctly pass it in that I can't find?
UPL.Init(ProjectArches, true, RelativeEnginePath, BundlePath, ProjectDirectory, Config.ToString(), false);

return GenerateIOSPList(ProjectFile, Config, ProjectDirectory, bIsUE4Game, GameName, bIsClient, ProjectName, InEngineDir, AppDirectory, SdkVersion, UPL, BundleID, bBuildAsFramework, out bSupportsPortrait, out bSupportsLandscape, out bSkipIcons);
}

In the final call of GeneratedIOSList, it constructs the default plist content, reads from Additional Plist Data, and calls UPL to handle the plist content, with UPL being processed last.

It is important to note that in the GeneratedPList function, all UPL.xml files added across all modules are fetched via GeneratedIOSList, with these XML files merged together. The merge order is based on the order of AdditionalProperties, with the last added UPL executing last. If multiple UPL operations are used in a project, you need to pay attention to the order.

Intervening in the IPA Generation Process: Operating Plist

The ipa package for iOS always contains a plist file, which can be used to configure certain properties of the app. Apple’s developer documentation provides detailed descriptions of each supported key: iOS Keys

The default packaging in UE 4.25.1 generates a plist file like the following: info.plist. In some special requirements, elements need to be added, modified, or deleted from this plist.

In UE’s project settings, elements can be added to the plist. In Project Settings-Platform-iOS-Additional Plist data, a string can be entered, which will be inserted into the plist file:

1
<key>AdditionalElementAAA</key>\n<string>this key is a test element.</string>

The \n in the middle is a formatting code used for a new line.

If you want to modify or delete elements in the plist, you need to write logic using UPL (of course, UPL can also be used to add elements; this approach is recommended).

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<root>
<init>
<log text="UPL Example adding element to plist..."/>
</init>
<trace enable="true"/>
<iosPListUpdates>
<addElements tag="dict" once="true">
<key>AdditionalElementAAA</key>
<string>this key is a test element.</string>
</addElements>
</iosPListUpdates>
</root>

The above is used to add elements, and the content is the same as directly writing to Additional Plist data.

Iterating through the keys in plist:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<root>
<init>
<log text="UPL Example..."/>
</init>
<trace enable="true"/>
<iosPListUpdates>
<loopElements tag="dict">
<loopElements tag="$">
<setStringFromTag result="TagName" tag="$"/>
<setBoolIsEqual result="bIsKey" arg1="$S(TagName)" arg2="key"/>
<if condition="bIsKey">
<true>
<log text="$S(TagName):$S(TagValue)"/>
</true>
</if>
</loopElements>
</loopElements>

</iosPListUpdates>
</root>

Note: The current element is referenced by tag = "$".

During compilation, the following log will appear:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
UPL: /Users/buildmachine/UE4/Builds/lipengzha-PC1/C/BuildAgent/workspace/PackageFGameClient/FGame/Plugins/UPLExample/Source/UPLExample/ThirdParty/IOS/IOS_UPL.xml
UPL Init: None
UPL Example adding element to plist...
key : CFBundleURLTypes
key : CFBundleDevelopmentRegion
key : CFBundleDisplayName
key : CFBundleExecutable
key : CFBundleIdentifier
key : CFBundleInfoDictionaryVersion
key : CFBundleName
key : CFBundlePackageType
key : CFBundleSignature
key : CFBundleVersion
key : CFBundleShortVersionString
key : LSRequiresIPhoneOS
key : UIStatusBarHidden
key : UIFileSharingEnabled
key : UIRequiresFullScreen
key : UIViewControllerBasedStatusBarAppearance
key : UIInterfaceOrientation
key : UISupportedInterfaceOrientations
key : UIRequiredDeviceCapabilities
key : CFBundleIcons
key : CFBundleIcons~ipad
key : UILaunchStoryboardName
key : CFBundleSupportedPlatforms
key : MinimumOSVersion
key : ITSAppUsesNonExemptEncryption
key : NSLocationAlwaysAndWhenInUseUsageDescription
key : NSLocationWhenInUseUsageDescription
key : CFBundleURLName
key : CFBundleURLSchemes
key : CFBundlePrimaryIcon
key : CFBundleIconFiles
key : CFBundleIconName
key : UIPrerenderedIcon
key : CFBundlePrimaryIcon
key : CFBundleIconFiles
key : CFBundleIconName
key : UIPrerenderedIcon

Adding new elements is relatively simple, but deleting and modifying is more troublesome, requiring a traversal of all nodes and deleting the current element based on a match (note that the plist consists of key-value pairs; each <key></key> corresponds to a value element that must also be deleted, or the packaging will fail):

1
2
<key>BuildMachineOSBuild</key>
<string>19C57</string>

I wrote a convenient process to delete elements from the plist, allowing for multiple groups of elements to be easily removed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<?xml version="1.0" encoding="utf-8"?>
<root>
<init>
<log text="UPL Example adding element to plist..."/>
</init>
<trace enable="true"/>
<iosPListUpdates>
<addElements tag="dict" once="true">
<key>AdditionalElementA</key>
<string>this key is a AdditionalElementA element.</string>
<key>AdditionalElementB</key>
<string>this key is a AdditionalElementB element.</string>
</addElements>

<setString result="NeedDeleteKey_1" value="AdditionalElementA"/>
<setString result="NeedDeleteKey_2" value="AdditionalElementB"/>
<setInt result="loopSumNum" value="2"/>

<setInt result="loopCount" value="1"/>
<setBoolIsLessEqual result="loopRun" arg1="$I(loopCount)" arg2="$I(loopSumNum)"/>
<while condition="loopRun">
<log text="count:$I(loopCount) SearchKey:$S(NeedDeleteKey_$I(loopCount))"/>

<setBool result="bIsDeleteElement" value="false"/>
<loopElements tag="dict">
<loopElements tag="$">
<!-- delete value -->
<if condition="bIsDeleteElement">
<true>
<setBool result="bIsDeleteElement" value="false"/>
<log text="bIsDeleteElement is true!!!"/>
<setStringFromTag result="TagName" tag="$"/>
<setStringFromTagText result="TagValue" tag="$"/>
<log text="Delete element value,tagname:$S(TagName) value:$S(TagValue)"/>
<removeElement tag="$" once="true"/>
</true>
</if>

<!-- delete key -->
<setStringFromTag result="TagName" tag="$"/>
<setBoolIsEqual result="bIsKey" arg1="$S(TagName)" arg2="key"/>
<if condition="bIsKey">
<true>
<setStringFromTagText result="TagValue" tag="$"/>
<log text="tagname:$S(TagName) tagvalue:$S(TagValue)"/>
<setBoolIsEqual result="bIs_NeedDeleteKey_$I(loopCount)" arg1="$S(TagValue)" arg2="$S(NeedDeleteKey_$I(loopCount))"/>
<if condition="bIs_NeedDeleteKey_$I(loopCount)">
<true>
<log text="Match key $S(NeedDeleteKey_$I(loopCount))."/>
<log text="Delete element key,tagname:$S(TagName) value:$S(TagValue)."/>
<removeElement tag="$" once="true"/>
<setBool result="bIsDeleteElement" value="true"/>
</true>
</if>
</true>
</if>
</loopElements>
</loopElements>

<!--control loop end-->
<setIntAdd result="loopCount" arg1="$I(loopCount)" arg2="1"/>
<setBoolIsLessEqual result="loopRun" arg1="$I(loopCount)" arg2="$I(loopSumNum)"/>
<if condition="loopRun">
<true>
<log text="add loopCount to $I(loopCount)"/>
</true>
<false>
<log text="the loop is finished!"/>
</false>
</if>
</while>

</iosPListUpdates>
</root>

At the beginning of the script, two elements pairs are added, followed by the deletion code, with the following three lines being important to note:

1
2
3
<setString result="NeedDeleteKey_1" value="AdditionalElementA"/>
<setString result="NeedDeleteKey_2" value="AdditionalElementB"/>
<setInt result="loopSumNum" value="2"/>

The first two lines contain the variables for the elements to be deleted, where the values are the strings of the keys to be deleted. Note that the naming rule must start with NeedDeleteKey_.
The third line creates a variable named loopSumNum to keep track of how many element pairs need to be deleted. Here, I test deleting two, so its value is 2.

When packaging and building, this script will execute and produce the following output:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
UPL: /Users/buildmachine/UE4/Builds/lipengzha-PC1/C/BuildAgent/workspace/PackageFGameClient/FGame/Plugins/UPLExample/Source/UPLExample/ThirdParty/IOS/IOS_UPL.xml
UPL Init: None
UPL Example adding element to plist...
count:1 SearchKey:AdditionalElementA
tagname:key tagvalue:CFBundleURLTypes
tagname:key tagvalue:CFBundleDevelopmentRegion
tagname:key tagvalue:CFBundleDisplayName
tagname:key tagvalue:CFBundleExecutable
tagname:key tagvalue:CFBundleIdentifier
tagname:key tagvalue:CFBundleInfoDictionaryVersion
tagname:key tagvalue:CFBundleName
tagname:key tagvalue:CFBundlePackageType
tagname:key tagvalue:CFBundleSignature
tagname:key tagvalue:CFBundleVersion
tagname:key tagvalue:CFBundleShortVersionString
tagname:key tagvalue:LSRequiresIPhoneOS
tagname:key tagvalue:UIStatusBarHidden
tagname:key tagvalue:UIFileSharingEnabled
tagname:key tagvalue:UIRequiresFullScreen
tagname:key tagvalue:UIViewControllerBasedStatusBarAppearance
tagname:key tagvalue:UIInterfaceOrientation
tagname:key tagvalue:UISupportedInterfaceOrientations
tagname:key tagvalue:UIRequiredDeviceCapabilities
tagname:key tagvalue:CFBundleIcons
tagname:key tagvalue:CFBundleIcons~ipad
tagname:key tagvalue:UILaunchStoryboardName
tagname:key tagvalue:CFBundleSupportedPlatforms
tagname:key tagvalue:MinimumOSVersion
tagname:key tagvalue:ITSAppUsesNonExemptEncryption
tagname:key tagvalue:NSLocationAlwaysAndWhenInUseUsageDescription
tagname:key tagvalue:NSLocationWhenInUseUsageDescription
tagname:key tagvalue:AdditionalElementA
Match key AdditionalElementA.
Delete element key,tagname:key value:AdditionalElementA.
bIsDeleteElement is true!!!
Delete element value,tagname:string value:this key is a AdditionalElementA element.
tagname:key tagvalue:AdditionalElementB
tagname:key tagvalue:CFBundleURLName
tagname:key tagvalue:CFBundleURLSchemes
tagname:key tagvalue:CFBundlePrimaryIcon
tagname:key tagvalue:CFBundleIconFiles
tagname:key tagvalue:CFBundleIconName
tagname:key tagvalue:UIPrerenderedIcon
tagname:key tagvalue:CFBundlePrimaryIcon
tagname:key tagvalue:CFBundleIconFiles
tagname:key tagvalue:CFBundleIconName
tagname:key tagvalue:UIPrerenderedIcon
add loopCount to 2
count:2 SearchKey:AdditionalElementB
tagname:key tagvalue:CFBundleURLTypes
tagname:key tagvalue:CFBundleDevelopmentRegion
tagname:key tagvalue:CFBundleDisplayName
tagname:key tagvalue:CFBundleExecutable
tagname:key tagvalue:CFBundleIdentifier
tagname:key tagvalue:CFBundleInfoDictionaryVersion
tagname:key tagvalue:CFBundleName
tagname:key tagvalue:CFBundlePackageType
tagname:key tagvalue:CFBundleSignature
tagname:key tagvalue:CFBundleVersion
tagname:key tagvalue:CFBundleShortVersionString
tagname:key tagvalue:LSRequiresIPhoneOS
tagname:key tagvalue:UIStatusBarHidden
tagname:key tagvalue:UIFileSharingEnabled
tagname:key tagvalue:UIRequiresFullScreen
tagname:key tagvalue:UIViewControllerBasedStatusBarAppearance
tagname:key tagvalue:UIInterfaceOrientation
tagname:key tagvalue:UISupportedInterfaceOrientations
tagname:key tagvalue:UIRequiredDeviceCapabilities
tagname:key tagvalue:CFBundleIcons
tagname:key tagvalue:CFBundleIcons~ipad
tagname:key tagvalue:UILaunchStoryboardName
tagname:key tagvalue:CFBundleSupportedPlatforms
tagname:key tagvalue:MinimumOSVersion
tagname:key tagvalue:ITSAppUsesNonExemptEncryption
tagname:key tagvalue:NSLocationAlwaysAndWhenInUseUsageDescription
tagname:key tagvalue:NSLocationWhenInUseUsageDescription
tagname:key tagvalue:AdditionalElementAAA
tagname:key tagvalue:AdditionalElementB
Match key AdditionalElementB.
Delete element key,tagname:key value:AdditionalElementB.
bIsDeleteElement is true!!!
Delete element value,tagname:string value:this key is a AdditionalElementB element.
tagname:key tagvalue:CFBundleURLName
tagname:key tagvalue:CFBundleURLSchemes
tagname:key tagvalue:CFBundlePrimaryIcon
tagname:key tagvalue:CFBundleIconFiles
tagname:key tagvalue:CFBundleIconName
tagname:key tagvalue:UIPrerenderedIcon
tagname:key tagvalue:CFBundlePrimaryIcon
tagname:key tagvalue:CFBundleIconFiles
tagname:key tagvalue:CFBundleIconName
tagname:key tagvalue:UIPrerenderedIcon
the loop is finished!

As long as deletion is possible, the deleted elements can be re-added using addElements, thus achieving the modification purpose. ### Adding Frameworks for iOS

The Framework on iOS is somewhat similar to a static linked library, equivalent to a collection of .a + .h + resources packaged together. For a more specific description of the differences, see: Difference between iOS Library .a and .framework

In UE, using the example of integrating the iOS SSKeychain that operates the Keychain, PublicAdditionalFrameworks is used in the Module’s build.cs to add:

1
2
3
4
5
6
7
PublicAdditionalFrameworks.Add(
new Framework(
"SSKeychain",
"ThirdParty/IOS/SSKeychain.embeddedframework.zip",
"SSKeychain.framework/SSKeychain.bundle"
)
);

The first parameter for constructing the Framework is the name, the second is the framework path (relative to the Module), and the third is the bundle path of the Framework after extraction (if the framework does not have a bundle, this parameter can be ignored, and even if there is a bundle but this third parameter is not written, it seems like it also doesn’t cause any problems).

This can be verified by opening the SSKeychain.embeddedframework.zip file:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
SSKeychain.embeddedframework
└─SSKeychain.framework
│ Info.plist
│ SSKeychain

├─Headers
│ SSKeychain.h
│ SSKeychainQuery.h

├─Modules
module.modulemap

├─SSKeychain.bundle
│ └─en.lproj
│ SSKeychain.strings

└─_CodeSignature
CodeDirectory
CodeRequirements
CodeRequirements-1
CodeResources
CodeSignature

The path relative to the .framework must be filled in correctly; otherwise, it cannot be used because the packaging process will extract this zip and copy it to the package. If the path is specified incorrectly, it will not be copied.

1
[2020.05.14-11.04.48:324][988]UATHelper: Packaging (iOS):     [2/183] sh Unzipping : /Users/zyhmac/UE4/Builds/ZHALIPENG/C/Users/imzlp/Documents/UnrealProjectSSD/MicroEnd_423/Plugins/PlatformUtils/Source/PlatformUtils/ThirdParty/IOS/SSKeychain.embeddedframework.zip -> /Users/zyhmac/UE4/Builds/ZHALIPENG/D/UnrealEngine/Epic/UE_4.23/Engine/Intermediate/UnzippedFrameworks/SSKeychain/SSKeychain.embeddedframework

Note: Do not introduce the same third-party framework file simultaneously in two different modules; otherwise, the following error may occur (for example, if I introduced SSKeychain.embeddedframework.zip in plugin A, then introduced it in another plugin B within the same project):

1
Unable to merge actions producing SSKeychain.embeddedframework.extracted: prerequisites are different.

Common Issues for Testing iOS Packages

iOS UE4App Data Directory

Accessing the iOS program’s documents directory requires the App to enable file sharing. This can be enabled in UE’s Project Settings-Platform-IOS-File System:

Only after this is enabled can you access the application’s documents directory following packaging.

You can use iMaZing to access the iOS app’s documents directory, allowing you to create folders, copy files, etc., which is quite convenient.

UE has this directory structure on Win and Android platforms:

1
2
3
4
5
6
7
8
9
+---Engine
| +---Content
\---FGame
+---Content
| +---Movies
| \---Paks
\---Saved
+---Logs
\---Paks

On Windows, it is relative to the packaging directory, while on Android, it defaults to <Sdcard>/UE4Game/PROJECT_NAME/.

On iOS, this structure is relative to the App’s documents directory (these folders were manually created; there are no logs during Shipping):

If you want to mount pak, just place it in the corresponding directory structure, and you can access it through the FPaths API in the program.

Viewing iOS Device Logs on Windows

For Android devices, you can use adb logcat to capture logs, but it’s quite troublesome to view iOS logs since it requires a Mac.

However, after a bit of searching, I found a tool that allows you to view the current device logs in real-time on Windows: IOSLogInfo

After downloading, extract it, and executing sdsiosloginfo.exe will display output similar to logcat logs. If you have a Git bash environment, you can also use | for filtering.

Log Location for UE Projects on Mac

The log location for opening UE projects on MacOS is ~/Library/Logs/Unreal Engine/ProjectName, Locating Project Logs

Paklist Location for Packaging on Mac

The location of the Paklist generated during IPA packaging on Mac is ~/Library/Logs/Unreal Engine/LocalBuildLogs/BuildCookRun, which differs from Windows packaging.

The article is finished. If you have any questions, please comment and communicate.

Scan the QR code on WeChat and follow me.

Title:UE Development Notes: Mac/iOS
Author:LIPENGZHA
Publish Date:2020/09/01 21:45
Update Date:2023/09/08 09:47
Word Count:17k Words
Link:https://en.imzlp.com/posts/1948/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!